Skip to main content

Slightly alternative way to write the code

Project description

fnkit

PyPI version Python versions License: MIT Tests

Typed functional programming primitives for Python 3.14, built on native PEP 695 generics.

Option, Result, and State give you composable, type-checked alternatives to None checks, exception-driven error handling, and hand-rolled state threading, without a class hierarchy to inherit from and without losing type information along the way.

Why this exists

Most Python functional-programming libraries predate PEP 695 and still lean on TypeVar and Generic[T]. This library is built from the ground up on Python 3.14's native generic syntax (class Foo[T], def bar[T, E = Default]), structural Protocols instead of inheritance, and match statements for unwrapping values.

Features

  • Option[T]: a Just / Nothing pair for values that may or may not exist, replacing None-based optionality with a type-checked container.
  • Result[T, E]: an Ok / Err pair for computations that may fail, replacing exception-driven control flow with explicit, inspectable failure values.
  • State[S, A]: a composable wrapper around S -> (A, S) functions, for threading state through a pipeline without mutation or global variables.
  • Collection[T]: an iterable-backed container with map/bind/reduce, plus a sequence function that turns a Collection of Option/Result/State values inside-out into a single Option/Result/State of a Collection.
  • Monad transformers (o_bindt, r_bindt): compose State with Option or Result directly, so a stateful computation can short-circuit on Nothing or Err without manual unwrapping at every step.
  • map / bind / lift across all types, for a consistent functional interface regardless of which container you are working with.
  • Fully immutable: every type is a frozen, slotted dataclass. No shared mutable state, no reference cycles.
  • Structural typing throughout: Option, Result, and State are @runtime_checkable Protocols, so you can write your own conforming implementations without subclassing.

Requirements

Python 3.14 or later.

Installation

uv add fnkit

Quick examples

Option

from fnkit.option import Just, Nothing, Option, pure

def find_user(name: str) -> Option[str]:
    return Just(name) if name == "ada" else Nothing()

result = (
    find_user("ada")
    .map(str.upper)
    .bind(lambda name: Just(f"hello, {name}"))
    .just_or(lambda: "user not found")
)
print(result)  # hello, ADA

Result

from fnkit.result import Err, Ok, Result

def parse_positive(raw: str) -> Result[int, str]:
    value = int(raw)
    return Ok(value) if value > 0 else Err(f"{value} is not positive")

result = (
    parse_positive("42")
    .map(lambda n: n * 2)
    .map_err(lambda e: f"parse failed: {e}")
    .ok_or(lambda e: -1)
)
print(result)  # 84

State

from fnkit.state import State, pure

def push[T](item: T) -> State[list[T], None]:
    def run(stack: list[T]) -> tuple[None, list[T]]:
        return None, [*stack, item]
    return State(run)

def pop[T]() -> State[list[T], T]:
    def run(stack: list[T]) -> tuple[T, list[T]]:
        return stack[-1], stack[:-1]
    return State(run)

program = push(1).bind(lambda _: push(2)).bind(lambda _: pop())
value, final_state = program.run_state([])
print(value, final_state)  # 2 [1]

Combining State with Result or Option

from fnkit.result import Err, Ok, Result
from fnkit.state import State
from fnkit.transformers import r_bindt

def read_and_validate(state: State[int, Result[int, str]]) -> State[int, Result[int, str]]:
    def validate(value: int) -> State[int, Result[int, str]]:
        if value < 0:
            return State(lambda s: (Err("negative value"), s))
        return State(lambda s: (Ok(value * 2), s))

    return r_bindt(state, validate)

Collection and sequence

from fnkit.collection import Collection, sequence
from fnkit.result import Err, Ok, Result

def parse_positive(raw: int) -> Result[int, str]:
    return Ok(raw) if raw > 0 else Err(f"{raw} is not positive")

results = Collection(parse_positive(n) for n in (1, 2, 3))
combined = sequence(results)
print(combined)  # Ok(Collection([1, 2, 3]))

results_with_failure = Collection(parse_positive(n) for n in (1, -2, 3))
print(sequence(results_with_failure))  # Err("-2 is not positive")

Design notes

  • Nothing, Ok, and Err compare and hash by value, not by identity, since all three are plain frozen dataclasses.
  • State compares and hashes by function identity, since two closures cannot be compared for behavioral equality in general. Do not rely on State() == State() for anything other than the same object.
  • lift for Option and Result evaluates its first argument before its second, so when both sides are Nothing/Err, the first argument's failure is the one that propagates. This matches left-to-right short-circuiting.
  • sequence on an empty Collection cannot detect which monad to wrap the result in, since there is no element to inspect. Rather than introduce an Identity monad solely to make this one case type-correct, sequence(Collection(())) returns a bare Collection unchanged. This is a known, deliberate limitation: callers sequencing collections that might be empty should check for that case themselves before relying on the wrapped-monad return type.
  • Collection is a thin wrapper over any Iterable[T], so it inherits whatever rewindability (or lack of it) the underlying iterable has. Back it with a list or tuple and you can iterate, map, or bind over the same Collection repeatedly. Back it with a generator or any other one-shot iterator and the first traversal exhausts it, so a second .map(), .bind(), or for loop over the same instance silently yields nothing. Collection does not materialize or cache its contents on your behalf.

Running tests

uv sync --all-groups
make verify

Note that make verify runs ruff check --fix and ruff format, both of which rewrite files in place rather than only reporting violations. If you want a read-only check instead (what CI runs), use make verify_ci.

License

MIT. See LICENSE for the full text.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fnkit-0.6.0.tar.gz (6.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fnkit-0.6.0-py3-none-any.whl (7.9 kB view details)

Uploaded Python 3

File details

Details for the file fnkit-0.6.0.tar.gz.

File metadata

  • Download URL: fnkit-0.6.0.tar.gz
  • Upload date:
  • Size: 6.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fnkit-0.6.0.tar.gz
Algorithm Hash digest
SHA256 6566b00d4fac00c42048f70d28ee2d0a54aa69a3adae95a709ac5325a711097a
MD5 783cb20d8d8a894a04bdbf7beb323e23
BLAKE2b-256 86b81dc3ed0395eafb317d47b00d87caeb19305c9d3089e515e4733ff4e3d745

See more details on using hashes here.

File details

Details for the file fnkit-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: fnkit-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 7.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fnkit-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7dcebaca037cc98625c35c2ab99488f750b834019b09baf7679c62abcc5ef1e1
MD5 733aaab878b7e94eb281776f1093c539
BLAKE2b-256 9e61bfecd377be79be9252b13aa15c8978de40feb4bb91cdfa207d84667eac99

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page