Monad Transformers
funstruct.experimental.monadtransformer
Monad transformers — combine two monadic effects in a single pipeline.
Transformers exist because monads do NOT compose generically. If you have
Future[Option[User]], you can't bind through both layers at once —
you'd need to unwrap the Future, then pattern match the Option, at every step.
A transformer makes the nesting implicit. You write one flat pipeline;
the transformer peels both layers in bind.
Types
OptionT[F, A] = F[Option[A]] — absence + F's effects EitherT[F, E, A] = F[Either[E, A]] — typed errors + F's effects ReaderT[F, Ctx, A] = Ctx -> F[A] — shared environment + F's effects StateT[F, S, A] = S -> F[(S, A)] — threaded state + F's effects WriterT[F, W, A] = F[(A, W)] — accumulated output + F's effects
Example — OptionT flattens Future[Option[...]] into a single pipeline::
from funstruct.monad.option import Option, Some, Nothing
from funstruct.monad.future import Future
from funstruct.experimental.monadtransformer.option_t import OptionT
# These return different nested shapes:
def get_user(name: str) -> Future[Option[User]]:
return Future.pure(Option.from_optional(users.get(name)))
def get_age(user: User) -> Future[int]:
return Future.pure(3000)
def get_nickname(user: User) -> Option[str]:
return Option.from_optional(nicknames.get(user.name))
# Without transformer — manual unwrapping at every step:
# result = await get_user("me") # Future[Option[User]]
# match result:
# case Some(user):
# age = await get_age(user) # Future[int] — different shape!
# ...
# With OptionT — one flat pipeline, lift mismatched types:
@OptionT.do
def pipeline() -> OptionT[Future, str]:
user = yield OptionT(get_user("me")) # Future[Option[User]] — exact match
age = yield OptionT.lift_f(get_age(user)) # Future[int] → OptionT (missing Option layer)
nick = yield OptionT.from_option(get_nickname(user), Future) # Option[str] → OptionT (missing Future layer)
return f"{nick}{age}"
result: Option[str] = await pipeline().run() # unwrap at the boundary
Status: experimental. API may change.