If you ask why
tibia
is calledtibia
- i don't remember why 😄
tibia
provides several containers for values:
Value
for already computed (sync) values with pipe operator / fluent APIFuture
for not yet computed (async) values with pipe operator / fluent APIMaybe
is 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
)
FutureMaybe
is likeFuture
forValue
, but forMaybe
Result
is 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
FutureResult
is likeFuture
forValue
, 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 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 true
- 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 inMaybe
container (Some
if key is present,Empty
if 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 * y
Value.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
- tibia
s 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) -> str
With 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.inspect
for sync functionsFuture.map_async
&Future.inspect_async
for async functionsFuture.unwrap
for extracting contained value (must be awaited)
Additionally one can await
directly Future
without unwrap
:
_ = await future.unwrap()
# same as
_ = await future
This 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 = None
Imagine 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 intoSome
map_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
: raiseValueError
with 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
-True
if container isSome
, otherwiseFalse
is_empty
-True
if container isEmpty
, otherwiseFalse
is_some_and
-True
if container isSome
and passed predicate isTrue
(function that checks contained value), otherwiseFalse
is_empty_or
-True
if container isEmpty
or passed predicate isTrue
(function that check contained inSome
value), 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 intoSome
Maybe.from_value_when
- wraps value intoSome
if value satisfies passed predicate, otherwiseEmpty
Maybe.from_optional
- wraps value intoSome
if one is notNone
, otherwiseEmpty
Maybe.from_optional_when
- wraps value intoSome
if value is notNone
and satisfies passed predicate , otherwiseEmpty
Based on from_value
and from_optional
Maybe
provides 2 decorators for
functions:
Maybe.wraps
- wraps returned from function value viaMaybe.from_value
Maybe.wraps_optional
- wraps returned from function value viaMaybe.from_optional
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 forOk
containermap_err
/map_err_async
- mapping forErr
containermap_or
/map_or_async
- mapping and unwrapping forOk
containermap_err_or
/map_err_or_async
- mapping and unwrapping forErr
containerinspect
/inspect_async
- applying side-effect function forOk
containerinspect_err
/inspect_err_async
- applying side-effect function forErr
container
Also provides more options for unwrapping both containers:
expect
Ok
- returns container valueErr
- raisesValueError
with passed error message
unwrap
Ok
- returns container valueErr
- raisesValueError
with default error message"must be ok"
unwrap_or
Ok
- returns container valueErr
- returns passed default value
expect_err
Ok
- raisesValueError
with passed error messageErr
- returns container value
unwrap_err
Ok
- raisesValueError
with default error message"must be err"
Err
- returns container value
unwrap_err_or
Ok
- returns passed default valueErr
- returns container value
Many more options for validating containers and values:
is_ok
-True
ifOk
, otherwiseFalse
is_ok_and
/is_ok_and_async
-True
ifOk
and contained value satisfies passed predicate, otherwiseFalse
(returnsFuture[bool]
for async version)is_ok_or
/is_ok_or
-True
ifOk
orErr
contained value satisfies passed predicate, otherwiseFalse
(returnsFuture[bool]
for async version)is_err_
-True
ifErr
, otherwiseFalse
is_err__and
/is_err__and_async
-True
ifErr
and contained value satisfies passed predicate, otherwiseFalse
(returnsFuture[bool]
for async version)is_err__or
/is_err__or
-True
ifErr
orOk
contained value satisfies passed predicate, otherwiseFalse
(returnsFuture[bool]
for async version)
Provides 2 decorators:
Result.wraps
- simply always returnsOk
-wrapped function resultResult.safe
- if wrapped function:- returned value successfully - wraps it into
Ok
- raised exception in
exceptions
- wraps catches error intoErr
- raised exception not in
exceptions
- exception is raised (undesired behavior)
- returned value successfully - wraps it into
Same as Maybe
, Result
provides simple functions that can replace any method.
ResultMaybe
provides exactly the same API, but for "futurized" Result
value.