wird is a library that provides basic monads in python. Core idea is to provide
mechanics for writing purely python pipeline-styled code.
Why wird? Wird is a misspelling of Anglo-Saxon / Old North word "wyrd". It means fate, but not totally predefined, more like a consequence of previous deeds.
Before getting into wird API it's worth explaining concept of pipeline-styled code.
Mainly our code is imperative - we describe what we do to achieve some result in steps,
one by one. It's not worth to reject imperative code in favor of declarative one (where
we describe the result instead of steps for getting it), as most languages are generally
imperative and it's more convenient to provide better ways to write it.
Different languages provide pipelines in different forms. For example in C# or Java it is provided with so called Fluent API (sometimes method chaining). Example:
int[] numbers = [ 5, 10, 8, 3, 6, 12 ];
IEnumerable<int> evenNumbersSorted = numbers
.Where(num => num % 2 == 0)
.OrderBy(num => num);There we write some class that allows us to chain method execution in order to perform some action. This is quite nice approach, however it's not really extensible and does not suit to most of the business cases where we want to separate bits of logic into different entities.
Mostly this kind of syntax is used for builder pattern:
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
host.Run(); In functional languages you can find so called "pipe operator" - |>. Let's take a look
at simple case - we want to put to square some number, that convert that to string and
reverse it. In F# you might write that like:
let result = rev (toStr (square 512))Problem of this piece of code is that despite or algorithm is simple and direct, when we write code it steps are written in reverse order and we need to "unwrap" function calls.
With pipe operator same code becomes much more elegant:
let result = 512
|> square
|> toStr
|> revAll actions are written one-by-one in the same order as they executed. This is much more readable code.
Basically wird is written to provide this mechanic to python language in some
opinionated form inspired by Rust language.
Container for sync value that provides pipe-styled execution of arbitrary functions. Let's look at the example:
import operator
from wird import Value
res = (
Value(3)
.map(operator.add, 1) # 3 + 1 -> 4
.map(operator.mul, 3) # 4 * 3 -> 12
.map(operator.truediv, 2) # 12 / 2 -> 6
.inspect(print) # print 6.0 & pass next
.unwrap(as_type=int) # extract 6.0 from container
)Value is a simple wrapper around passed value with special methods (map /
map_async / inspect / inspect_async) that bind passed function to container value
(read as invoke / apply). Thus it is basically is a simplest monad.
Value provides the following interface:
Value.unwrap- method for extracting internally stored value with optional type casting (only for type checker, not actual casting happens)Value.map- binding method for sync functionsValue.map_async- binding method for async functionsValue.inspect- binding method for sync side-effect functionsValue.inspect_async- binding method for async side-effect functions
Main different between map and inspect is that map wraps the result of the
executed function into Value container and inspect just invokes function passing
stored value next. If stored value is mutable, inspect can be used to modify it via
side effect.
Container for async values. It is similar to Value and provides nearly the same
interface. When we invoke any of async methods in Value we actually return Future
container, as now stored value is computed asynchronously and requires await.
import operator
from wird import Value
async def mul_async(x: int, y: int) -> int:
return x * y
async def truediv_async(x: int, y: int) -> float:
return x / y
async def main():
res = await (
Value(3)
.map(operator.add, 1) # 3 + 1 -> 4 (Value)
.map_async(mul_async, 3) # 4 * 3 -> 12 (Future)
.map_async(truediv_async, 2) # 12 / 2 -> 6.0 (Future)
.inspect(print) # print 6.0 & pass next (Future)
.unwrap() # extract awaitable 6.0 from container
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())Future provides the following interface:
Future.unwrap- extract internally stored awaitable valueFuture.map- binding method for sync functionsFuture.map_async- binding method for async functionsFuture.inspect- binding method for sync side-effect functionsFuture.inspect_async- binding method for async side-effect functionsFuture.from_- static method for creating awaitable object from sync value
Also Future is awaitable by itself, so one can just await Future itself instead of
calling Future.unwrap, but to stay uniform it is recommended to use Future.unwrap.
Despite Value and Future, Maybe is not a single container, but rather a pair of
containers - Some and Empty. Each resembles additional property of data - its
presence.
Some indicates that data is present allowing it to be processed. Empty on the other
hand marks that there is not data and we can't perform execution ignoring that.
Basically it hides explicit is None checks, taking it as internal rule of function
mapping.
Possible relevant case of usage is database patch / update operations, when we intentionally want to provide some abstract interface that allows optional column update. For example we store in SQL database following data structure:
from dataclasses import dataclass
from datetime import date
@dataclass
class Customer:
uid: int
first_name: str
second_name: str
birthdate: date | None = NoneWe provide HTTP route to update this entity in DB. If we've provided a field in request
body, then this field must be updated. Commonly one will make each field in DTO (except
for ID) optional with default None value, but what to do with birthdate? When
parsing we will propagate default None so we do not know if this None was passed
explicitly or we've implicitly set it via DTO default.
Maybe allows to explicitly separate this cases, allowing us to have None as present value:
from dataclasses import dataclass
from datetime import date
from wird import Empty, Maybe
@dataclass
class CustomerUpdate:
uid: int
first_name: Maybe[str] = Empty()
second_name: Maybe[str] = Empty()
birthdate: Maybe[date | None] = Empty()Thus when birthdate is Empty we know that we do not have to update this column at
all, and when it is Some we can safely set None as desired value.
Maybe provides the following interface:
Maybe.unwrap- extract internally stored value onSome, raiseEmptyUnwrapErroronEmptyMaybe.unwrap_or- extract internally stored value onSomeor return passed replacement value onEmptyMaybe.unwrap_or_none- extract internally stored value onSomeor returnNoneonEmptyMaybe.unwrap_or_else- extract internally stored value onSomeor return result of execution of factory function for replacement value onEmptyMaybe.unwrap_or_else_async- same asMaybe.unwrap_or_else, but for async factory functionMaybe.map- binding method for sync functions, applies only onSomeMaybe.map_async- same asMaybe.map, but for async functionsMaybe.inspect- binding method for sync side-effect functions, applies only onSomeMaybe.inspect_async- same asMaybe.inspect, but for async functionsMaybe.and_- logical AND for 2Maybevalues, replaces selfMaybewith passedMaybeif first one isSomeMaybe.and_then- same asMaybe.map, but for sync functions that returnMaybeMaybe.and_then_async- same asMaybe.and_then, but for async functionsMaybe.or_- logical OR for 2Maybevalues, replaces selfMaybewith passedMaybeif first one isEmptyMaybe.or_else- replacesEmptywithMayberesult of passed sync functionMaybe.or_else_async- same asMaybe.or_else, but for async functionsMaybe.is_some-TrueonSomecontainerMaybe.is_some_and-TrueonSomecontainer and passed predicate beingTrueMaybe.is_some_and_async- same asMaybe.is_some_and, but for async predicatesMaybe.is_empty-TrueonEmptycontainerMaybe.is_empty_or-TrueonEmptycontainer or passed predicate beingTrueMaybe.is_empty_or_async- same asMaybe.is_empty_or, but for async predicatesMaybe.filter- if predicate isFalsereplacesMaybewithEmptyMaybe.filter_async- same asMaybe.filter, but for async predicates
In order to provide seamless experience, instead of making developer to work with
Future[Maybe[T]] we provide FutureMaybe container that provides exactly the same
interface as sync Maybe. Worth noting that FutureMaybe is awaitable, like Future,
and returns internally stored Maybe instance.
Also in some cases one might need point-free versions of Maybe interface methods, so
one can access them via maybe module. For FutureMaybe point-free functions one can
use future_maybe module.
Exception handling is one of the most important tasks in development. We often face
cases when invocation of some logic can lead to Exception raise. In python default
handling mechanism is try - except - finally block, which is actually just another
for of if statement.
Worst thing about this approach is that commonly in python the only way to know that function can raise an exception is documentation (which is not always written or written good). There is no explicit mechanism to tell LSP / type checker / linter, that this specific function needs exception handling.
Result monad provides another approach, which is common for Rust and Go developers -
let's return exceptions instead of raising them. Thus we can explicitly tell that soma
action can fail and requires edge-case handling.
Like Maybe, Result is just a protocol and has 2 implementations:
Ok- container indicating that calculation succeededErr- container indicating that calculation failed
Simplest case of using Result is division:
from wird import Result, Ok, Err
def try_div(a: int, b: int) -> Result[float, ZeroDivisionError]:
if b == 0:
return Err(ZeroDivisionError())
return Ok(a / b)There we explicitly tell that division operation can lead to failure and even pinpoint specific type of error.
Result provides the following interface:
Result.unwrap- extract internally stored value ofOkor raiseErrUnwrapErrorResult.unwrap_or- extract internally stored value ofOkor return otherResult.unwrap_or_else- extract internally stored value ofOkor return closure resultResult.unwrap_or_else_async- same asResult.unwrap_or_else, but for async closuresResult.unwrap_err- same asResult.unwrap, but forErrResult.unwrap_err_or- same asResult.unwrap_err_or, but forErrResult.unwrap_err_or_else- same asResult.unwrap_err_or_else, but forErrResult.unwrap_err_or_else_async- same asResult.unwrap_err_or_else_async, but forErrResult.map- binding method forOkResult.map_async- same asResult.map, but for async functionsResult.inspect- binding side-effect method forOkResult.inspect_async- same asResult.inspect_async, but for async functionsResult.map_err- same asResult.map, but forErrResult.map_err_async- same asResult.map_async, but forErrResult.inspect_err- same asResult.inspect, but forErrResult.inspect_err_async- same asResult.inspect_async, but forErrResult.and_- logical AND, replaces currentResultwith passed onOkResult.and_then- same asResult.map, but for functions returningResultResult.and_then_async- same asResult.and_then, but for async functionsResult.or_- logical OR, replaces currentResultwith passed onErrResult.or_else- same asResult.map_err, but for functions returningResultResult.or_else_async- same asResult.or_else, but for async functionsResult.is_ok-TrueonOkResult.is_ok_and-TrueonOkand predicateTrueResult.is_ok_and_async- same asResult.is_ok_and, but for async predicateResult.is_ok_or-TrueonOkorErrpredicateTrueResult.is_ok_or_async- same asResult.is_ok_or, but for async predicateResult.is_err-TrueonErrResult.is_err_and-TrueonErrand predicateTrueResult.is_err_and_async- same asResult.is_err_and, but for async predicateResult.is_err_or-TrueonErrorOkpredicate `TrueResult.is_err_or_async- same asresult.is_err_or, but for async predicate
In the same manner as with Maybe we wird provides:
FutureResultas seamless adapter forFuture[Result]- point-free
ResultAPI inwird.resultmodule - point-free
FutureResultAPI inwird.future_resultmodule