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.1

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.1
File Size Uploaded
funstruct-1.0.1.tar.gz 140.4 kB Details

Built distribution (wheel)

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

Total release size: 187.6 kB

Release files / funstruct-1.0.1.tar.gz

Download URL funstruct-1.0.1.tar.gz
Size 140.4 kB
Tags Source
SHA-256 checksum
How to use checksums
a8fddeb154e86e8c0c9048820439868035b6e5570eb1169c4843f5503db4e049
BLAKE2b-256 checksum
How to use checksums
f9c9f474ad0d128aae1e2a89f153ff270dbca5e6c5e0e877f9d56b284ab34e3e
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.1-py3-none-any.whl

Download URL funstruct-1.0.1-py3-none-any.whl
Size 47.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b6cbd9d5552474c62e19071270d129d04c2cb84628d256d0aef1f8ec43d5d763
BLAKE2b-256 checksum
How to use checksums
fece1f6ccbd871c00e31b8578adfadd0985b66a6091eb9d7ed885d7d415b632b
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

1.0.2

2 release files

This release

1.0.1 This release

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