Skip to main content

funstruct

A helpful collection of functional utilities.

Install

pip install funstruct || uv add funstruct

Functional Primer

Type Class Hierarchy

Semigroup              Functor
    │                      │
 Monoid              Applicative
                           │
                         Monad
                           │
                    MonadTransformer

Diagrams

Semigroup — associative combine (+ being the canonical 'combine' operation)

A ─┐
    ├──( + )──> A
A ─┘

Monoid — semigroup with an identity element

A ─┐
    ├──( + )──> A       (+ identity = A)
A ─┘

Functor — transform the value inside a context

F[A] ---( f: A -> B )---> F[B]

Applicative — combine independent computations

F[A] ─┐
       ├──> F[(A, B)]
F[B] ─┘

Monad — sequence computations that produce new contexts

F[A] ---( f: A -> F[B] )---> F[B]
@dataclass(frozen=True)
class Semigroup:
    typ: type
    combine: Callable  # (A, A) -> A

@dataclass(frozen=True)
class Monoid(Semigroup):
    typ: type
    combine: Callable  # (A, A) -> A
    empty: object      # identity element

class Functor(ABC):
    def map(self, f) -> Functor: ...

class Applicative(Functor):
    def pure(cls, value) -> Applicative: ...
    def ap(self, other) -> Applicative: ...
    def __add__ = ap  # alias

class Monad(Applicative):
    def bind(self, f) -> Monad: ...
    def do(cls, gen_fn) -> Monad: ...
    def __rshift__ = bind  # >>

class MonadTransformer(Monad, Generic[_F, _A]):
    def lift_f(cls, inner: _F) -> MonadTransformer: ...
    def and_then(self, other) -> MonadTransformer: ...
# Multiple semigroups for the same type:
int_add = Monoid(typ=int, combine=lambda a, b: a + b, empty=0)
int_mul = Monoid(typ=int, combine=lambda a, b: a * b, empty=1)

~ Scala equivalent

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

trait Monoid[A] extends Semigroup[A] {
  def empty: A
}

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](a: A): F[A]
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
}

trait Monad[F[_]] extends Applicative[F] {
  def bind(fa: F[A])(f: A => F[B]): F[B]
}

Implementations

Typeclass Implementations
Functor Tree, frozendict, + all below
Applicative Validated, + all below
Monad Option, Either, State, Reader, Writer, CList
MonadTransformer ReaderT, StateT, EitherT, OptionT, WriterT
Type What it models
Option[A] Value might not exist
Either[E, A] Value or typed error
Result[A] (alias) Either[Exception, A] + @Try decorator
State[S, A] Stateful computation
Reader[Ctx, A] Shared environment
Writer[W, A] Accumulated output
Validated[E, A] Error accumulation (applicative, not monad)
Future[E, A] Lazy async + typed error
CList[A] Persistent singly-linked list
Tree[A] Immutable binary tree (functor only)
frozendict[K, V] Persistent HAMT dictionary

Monad Transformers

A transformer combines effects by wrapping one monad inside another.

ReaderT[F, Ctx, A]  =  Ctx -> F[A]         (environment + F's effects)
StateT[F, S, A]     =  S -> F[(S, A)]      (state + F's effects)
EitherT[F, E, A]    =  F[Either[E, A]]     (errors + F's effects)
OptionT[F, A]       =  F[Option[A]]        (absence + F's effects)
WriterT[F, W, A]    =  F[(A, W)]           (output + F's effects)

Why transformers? Monads don't compose automatically. If you need config + errors + logging, you'd manually unwrap 3 nested layers at every step. Transformers flatten that into one bind:

# Without transformer — nested pattern matching at every step:
result = fetch_user(id)  # Either[Err, Option[User]]
match result:
    case Left(e):
        ...  # handle error
    case Right(Nothing()):
        ...  # handle absence
    case Right(Some(user)):
        ...  # finally, the value

# With OptionT — one flat pipeline:
pipeline = (
    OptionT(fetch_user(id))
    .bind(lambda user: OptionT(get_email(user)))
    .map(lambda email: email.upper())
)

Laws

Every implementation must satisfy these mathematical laws:

Semigroup

  • Associativity: (a + b) + c == a + (b + c)

Monoid

  • Left identity: empty + a == a
  • Right identity: a + empty == a

Functor

  • Identity: fa.map(id) == fa
  • Composition: fa.map(f).map(g) == fa.map(g ∘ f)

Applicative

  • Identity: pure(id).ap(v) == v
  • Homomorphism: pure(f).ap(pure(x)) == pure(f(x))
  • Interchange: u.ap(pure(y)) == pure(λf. f(y)).ap(u)
  • Composition: pure(∘).ap(u).ap(v).ap(w) == u.ap(v.ap(w))

Monad

  • Left identity: pure(a).bind(f) == f(a)
  • Right identity: m.bind(pure) == m
  • Associativity: m.bind(f).bind(g) == m.bind(λx. f(x).bind(g))

Why no IO type?

In Haskell, IO exists because the language is purely functional — there is no way to perform side effects without wrapping them in the IO monad. The type system enforces purity: if a function doesn't return IO, it cannot touch the network, filesystem, or mutable state.

Python has no such constraint. Any function can perform side effects at any time. An IO wrapper in Python would be:

  1. Unenforceable — nothing stops you from doing I/O outside the wrapper. The type system can't prevent print() in a "pure" function.
  2. Purely ceremonial — it adds a wrapper you must manually construct and unwrap, but provides no guarantee. It's a comment dressed as a type.
  3. Redundant with async — Python's async/await already separates "description of a computation" from "execution of that computation," which is most of what IO provides in Haskell.

Instead, funstruct provides:

  • Either[E, A] — for operations that might fail (the error is a value)
  • Future[E, A] — for async operations that might fail (lazy, composable)
  • @Try / @TryAsync — for wrapping exception-throwing code at boundaries

These give you the composition benefits of monadic pipelines where they matter (error handling, async sequencing) without pretending Python is something it isn't.

Release files for funstruct 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for funstruct 1.0.2
File Size Uploaded
funstruct-1.0.2.tar.gz 140.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for funstruct 1.0.2
File Interpreter ABI Platform
funstruct-1.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 188.0 kB

Release files / funstruct-1.0.2.tar.gz

Download URL funstruct-1.0.2.tar.gz
Size 140.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f1070dd9a94c728fc92234f4af18065f87f4bdf03286e65ec3827a685f98a79b
BLAKE2b-256 checksum
How to use checksums
91a296b5990a13836c71cc0362cb31f76aa35fec3b1491def4db2c0270863637
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.

Transparency log

Release files / funstruct-1.0.2-py3-none-any.whl

Download URL funstruct-1.0.2-py3-none-any.whl
Size 47.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f5027bb5ca472994d4eecdde2e3702fa7d17f6e2f224a78cc6865e23da5b5a67
BLAKE2b-256 checksum
How to use checksums
c5733995c2c32bb0cf40a768f0a9f50901e4b3256c956db282b079c4734fd97d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.2 This release

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page