If you ask why
tibiais calledtibia- i don't remember why 😄
tibia provides several containers for values:
Valuefor already computed (sync) values with pipe operator / fluent APIFuturefor not yet computed (async) values with pipe operator / fluent APIMaybeis well-known monad for present and non-present already competed values with pipe operator / fluent API; provides 2 containers:Some- value is present and actually contained insideEmpty- nothing is contained (something like pythonNone)
FutureMaybeis likeFutureforValue, but forMaybeResultis well-known monad for successful or failed contained value states with pipe operator / fluent API; provides 2 containers:Ok- value is successfully computedErr- failed to compute value
FutureResultis likeFutureforValue, but forMaybe
Also tibia provides some utilitarian functions for Iterable and Mapping in
pipeline-based context:
Iterable- eager
map- applies passed function to each element of iterable eagerlyfilter- filter values in iterable with passed predicate eagerlyreduce&reduce_to- aggregates iterable values into single valuesort_asc&sort_desc- sort iterable valuestake- take first or last N values from iterable (can fail on infinite iterable)first- take first value or defaultskip- skip first or last N values from iterable (can fail on infinite iterable)join- flatten iterable of iterables into single iterable
- async (aio)
map- applies passed async function to each element of iterablefilter- filters elements in iterable with async predicate functiongroup_by- groups items of iterable with passed function
- lazy
map- applies passed async function to each element of iterable lazilyfilter- filters elements in iterable with async predicate function lazilytake_while- takes elements from iterable until predicate is trueskip_while- skips elements from iterable until predicate is truejoin- flattens iterable of iterables lazily
- threaded
map- applies passed sync function to each element of iterable in threadsfilter- filters in threads elements of iterable
- eager
Mapping- value
map- applies function to each value of mappingfilter- filters mapping pairs based on valueiterate- returns iterable of values (lazy as generator)set- set value to keyget- get value by key (can raiseKeyError)get_or- get value or passed default by keymaybe_get- get value by key inMaybecontainer (Someif key is present,Emptyif not)
- key
map- applies function to each key of mappingfilter- filters mapping pairs based on keyiterate- returns iterable of keys (lazy as generator)
- item
map- maps key-value pair to new key-value pairmap_to_value- maps value based on key-value pairmap_to_key- maps key based on key-value pairfilter- filters mapping elements based on key-value pairsiterate- returns iterable of key-value pairs (lazy as generator of tuples)
- value
Value is a simple container that brings to python so called pipe operator.
So how to use it? Put some concrete value to the container, for example a number. Now instead of passing this value as an argument to a function pass a function as some action that you want to be performed on this value:
_ = (
Value(1)
.map(add, 1)
.map(multiply_by, 3)
.map(subtract, 10)
.map(multiply_by, -1)
.inspect(print)
.unwrap()
)Thus we build a declarative chain of actions we perform on some piece of data.
Value.map is method that invokes passed function on contained value. It
supports not only single-argument functions, but actually any function with only
one requirement: contained value must be the first argument for the
function. Other arguments can be passed as *args and/or **kwargs to the
Value.map method.
So image we have a Value[int]. The following example function will be
completely valid for Value.map:
# simple single-argument function, contained value will be x
def add_one(x: int) -> int:
return x + 1
# 2 argument function, contained value will be x again, and argument for y must
# be passed following the function argument to map method
def add(x: int, y: int) -> int:
return x + y
# again 2 argument function, but in this case y argument can be ommited in map
def multiply_by(x: int, y: int = 1) -> int:
return x * yValue.map returns new Value container for data returned from the passed
function, thus with Value.map method chaining one can build pipelines on data.
Value.map works only with synchronous functions. For asynchronous functions
use Value.map_async.
Value.map_async is direct analogue of Value.map but for async functions.
Main difference is that instead of Value it returns Future - tibias simple
container over coroutine (Future is discussed further, but most things valid
for Value are valid for Future and their interface are very similar).
Value.inspect has nearly the same signature as Value.map with only
difference: it does not wrap returned from the passed function value into new
Value container, but returns the current self Value container.
Simply this is function for making side-effects - something we want to happen,
but don't care about result. In the example above Value.inspect was used to
print contained in container value. print returns None, so if we've used
Value.map we would lost the data, but with Value.inspect we just performed
an action and continued working on already contained data.
Like Value.map can be used only with synchronous functions.
Like Value.map_async is analogue of Value.map for async function that
returns Future containers Value.inspect_async is the same for
Value.inspect.
Method for extracting value from container if one is not further needed.
Decorator that changes signature of wrapped function by containing returned
value in Value container. For example initial signature was:
fn: (int, int, str) -> strWith Value.wraps decorator applied it becomes:
fn: (int, int, str) -> Value[str]Future is direct analogue of Value, but for values that are not calculated
(awaited) yet. For JS developers it might be widely know as Promise. It has
the same as Value interface:
Future.map&Future.inspectfor sync functionsFuture.map_async&Future.inspect_asyncfor async functionsFuture.unwrapfor extracting contained value (must be awaited)
Additionally one can await directly Future without unwrap:
_ = await future.unwrap()
# same as
_ = await futureThis is just a shortcut, but I would generally recommend explicit unwrapping and waiting of contained value.
Maybe is an alternative for python Optional. At first glance it might seem
that it is not needed, but it provides to apply functions without checks on
emptiness and also can consider None as actual value.
It consists of 2 containers:
Some- indicates that value is present (even if one isNone)Empty- indicates that there is no value
For example let's imagine one is creating data structure for updating some data (for example table in DB). Naive approach would be to create some class with all-optional fields:
class UpdateUser:
first_name: str | None = None
second_name: str | None = None
birthdate: datetime | None = NoneImagine that birthdate is nullable in table we want to update. If we get this
structure with None in birthdate field we cannot determine whether we want
to set it to null or we do not want to perform any update on this column.
With Maybe this problem goes away. Empty state tells us that we do not want
to perform action and Some unambiguously tells us that we want to set column
to contained value:
class UpdateUser:
first_name: Maybe[str] = Empty()
second_name: Maybe[str] = Empty()
birthdate: Maybe[datetime | None] = Empty()With this approach it is much more clear what each value means and what is valid state.
For making pipelines Maybe & FutureMaybe like Value & Future provide the
following methods:
map/map_async- apply contained value to function if one is present and wrap result intoSomemap_or/map_or_async- apply contained value to function if one is present and return result of function directly or replace it with passed default valueinspect/inspect_async- apply function as side-effect if value is present ignoring result of passed function and return current container
Imagine we apply function that returns R:
| Container | Method | Returns |
|---|---|---|
Ok[T] |
map |
Ok[R] |
Ok[T] |
map_async |
FutureMaybe(Ok[R]) |
Empty |
map |
Empty |
Empty |
map_async |
FutureMaybe(Empty) |
Ok[T] |
map_or |
R |
Ok[T] |
map_or_async |
Future[R] |
Empty |
map_or |
R from default |
Empty |
map_or_async |
Future[R] from default |
Ok[T] |
inspect |
Ok[T] |
Ok[T] |
inspect_async |
FutureMaybe(Ok[T]) |
Empty |
inspect |
Empty |
Empty |
inspect_async |
FutureMaybe(Empty) |
Mainly one would use map and map_async to perform desired actions without
thinking if value is present, some side-effect functions (for debugging for
example) with inspect & inspect_async and than unwrap value with map_or or
map_or_async methods (or via Unwrapping API).
In order to extract value from container one can use one of the following methods:
expect- if
Some: return contained value - if
Empty: raiseValueErrorwith passed error message
- if
unwrap- same asexpect, but uses built-in error message"must be some"unwrap_or- if
Some: return contained value - if
Empty: return passed default value
- if
unwrap_or_none- if
Some: return contained value - if
Empty: returnNone
- if
In order to check / validate contained value on can use following methods:
is_some-Trueif container isSome, otherwiseFalseis_empty-Trueif container isEmpty, otherwiseFalseis_some_and-Trueif container isSomeand passed predicate isTrue(function that checks contained value), otherwiseFalseis_empty_or-Trueif container isEmptyor passed predicate isTrue(function that check contained inSomevalue), otherwiseFalse
For example imagine we have is_even predicate:
| Container | Method | Returns |
|---|---|---|
Ok(2) |
is_some_and |
True |
Ok(1) |
is_some_and |
False |
Empty() |
is_some_and |
False |
Ok(1) |
is_empty_or |
False |
Ok(2) |
is_empty_or |
True |
Empty() |
is_empty_or |
True |
is_some_and & is_empty_or methods also have is_some_and_async &
is_empty_or_async alternatives for async predicates, as base versions work
only with sync functions. Async alternatives return Future[bool] for further
piping if needed.
There are a few static function in Maybe class used for constructing Maybe
containers:
Maybe.from_value- always wraps value intoSomeMaybe.from_value_when- wraps value intoSomeif value satisfies passed predicate, otherwiseEmptyMaybe.from_optional- wraps value intoSomeif one is notNone, otherwiseEmptyMaybe.from_optional_when- wraps value intoSomeif value is notNoneand satisfies passed predicate , otherwiseEmpty
Based on from_value and from_optional Maybe provides 2 decorators for
functions:
wraps- wraps returned from function value viaMaybe.from_valuesafe- wraps returned from function value viaMaybe.from_optional
One might also find
Maybe.wraps&Maybe.wraps_optionalstatic methods, but they are deprecated and not intended to be used.
All discussed above methods can also be used as functions contained in
tibia.maybe module. They can be found useful when working with iterables of
Maybe for example (for massive filtering and unwrapping without loosing type
hints).
FutureMaybe provides exactly the same API, but for "futurized" Maybe value.
Result & FutureResult monads provide the ability for indicating computation
success and error states without implicit error raises. Yes, yes, raising errors
is actually implicit way, that highly resembles to goto operator widely
forbidden or not implemented for increasing code complexity exponentially. With
Result one does not raise exception, but explicitly returns it (like Rust and
Go do for example).
It consists of 2 containers:
Ok- indicates successful resultErr- indicates failed result
Both of the containers store some value, Ok - what was actually computed,
Err - some error representation (not always Exception).
It is a bit extended relative to Maybe or Value and consist of the following
methods:
map/map_async- mapping forOkcontainermap_err/map_err_async- mapping forErrcontainermap_or/map_or_async- mapping and unwrapping forOkcontainermap_err_or/map_err_or_async- mapping and unwrapping forErrcontainerinspect/inspect_async- applying side-effect function forOkcontainerinspect_err/inspect_err_async- applying side-effect function forErrcontainer
Also provides more options for unwrapping both containers:
expectOk- returns container valueErr- raisesValueErrorwith passed error message
unwrapOk- returns container valueErr- raisesValueErrorwith default error message"must be ok"
unwrap_orOk- returns container valueErr- returns passed default value
expect_errOk- raisesValueErrorwith passed error messageErr- returns container value
unwrap_errOk- raisesValueErrorwith default error message"must be err"Err- returns container value
unwrap_err_orOk- returns passed default valueErr- returns container value
Many more options for validating containers and values:
is_ok-TrueifOk, otherwiseFalseis_ok_and/is_ok_and_async-TrueifOkand contained value satisfies passed predicate, otherwiseFalse(returnsFuture[bool]for async version)is_ok_or/is_ok_or-TrueifOkorErrcontained value satisfies passed predicate, otherwiseFalse(returnsFuture[bool]for async version)is_err_-TrueifErr, otherwiseFalseis_err__and/is_err__and_async-TrueifErrand contained value satisfies passed predicate, otherwiseFalse(returnsFuture[bool]for async version)is_err__or/is_err__or-TrueifErrorOkcontained value satisfies passed predicate, otherwiseFalse(returnsFuture[bool]for async version)
Provides 2 decorators:
wraps- simply always returnsOk-wrapped function resultsafe-try-exceptsExceptionand returnsOkifExceptionwas not raised, otherwise wrapsExceptionintoErrsafe_from- same assafebut firstly it excepts a tuple ofExceptiontypes to be excepted intry-exceptblock
One might also find
Result.wraps&Result.safestatic methods, but they are deprecated and not intended to be used.
Same as Maybe, Result provides simple functions that can replace any method.
ResultMaybe provides exactly the same API, but for "futurized" Result value.