Basic monads for implementing pipe-styled processing
Project description
wird
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.
Pipelines
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
|> rev
All 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.
Monads
Value
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.
Future
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.
Maybe
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 = None
We 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wird-1.1.0.tar.gz.
File metadata
- Download URL: wird-1.1.0.tar.gz
- Upload date:
- Size: 30.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea19aca3d1ff44e2fe44632dad80c191f0cdfbfc227c31efdc8d9ee4783afd71
|
|
| MD5 |
4251b1a791473eb1f15dd0b15b09a804
|
|
| BLAKE2b-256 |
ae8c147516659b1ffea20631345155f6f9b84fe128d561ebf82b4eb42ac57845
|
File details
Details for the file wird-1.1.0-py3-none-any.whl.
File metadata
- Download URL: wird-1.1.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb1646f1c8e088eace08f735ecfb686a7f66ac6af2c74c88a1f2b4dab0c7a467
|
|
| MD5 |
3b4bdd66d411bf73cb07ad9de2905438
|
|
| BLAKE2b-256 |
4ba6c42540dd502a72f5da11f634adcee413056a5114a4ff8d4f5b402be4f419
|