Introduction
trcks is a Python library that allows
railway-oriented programming (ROP)
in two different type-safe programming styles.
Railway-oriented programming (ROP) styles
The following subsections demonstrate
both styles of railway-oriented programming (ROP)
supported by trcks.
Object-oriented style
The object-oriented style is based on method chaining,
as demonstrated by the get_subscription_fee_by_email function
in the following example.
Example
>>> from typing import Literal
>>> from trcks import Result
>>> from trcks.oop import Wrapper
>>>
>>> UserDoesNotExist = Literal["User does not exist"]
>>> UserDoesNotHaveASubscription = Literal["User does not have a subscription"]
>>> FailureDescription = UserDoesNotExist | UserDoesNotHaveASubscription
>>>
>>> def get_user_id(user_email: str) -> Result[UserDoesNotExist, int]:
... if user_email == "erika.mustermann@domain.org":
... return "success", 1
... if user_email == "john_doe@provider.com":
... return "success", 2
... return "failure", "User does not exist"
>>>
>>> def get_subscription_id(user_id: int) -> Result[UserDoesNotHaveASubscription, int]:
... if user_id == 1:
... return "success", 42
... return "failure", "User does not have a subscription"
>>>
>>> def get_subscription_fee(subscription_id: int) -> float:
... return subscription_id * 0.1
>>>
>>> def get_subscription_fee_by_email(user_email: str) -> Result[FailureDescription, float]:
... return (
... Wrapper(core=user_email)
... .map_to_result(get_user_id)
... .map_success_to_result(get_subscription_id)
... .map_success(get_subscription_fee)
... .core
... )
>>>
>>> get_subscription_fee_by_email("erika.mustermann@domain.org")
('success', 4.2)
>>> get_subscription_fee_by_email("john_doe@provider.com")
('failure', 'User does not have a subscription')
>>> get_subscription_fee_by_email("jane_doe@provider.com")
('failure', 'User does not exist')
Note
- The generic type
trcks.Resultallows domain errors to become part of a function's return type (subject to static type checking). - The class
trcks.oop.Wrapperprovides a convenient way to chaintrcks.Result-returning functions and "regular" functions (in a type-safe way). - The
map*andtap*methods oftrcks.oop.Wrapper(and the other wrapper classes) accept extra positional and keyword arguments, which are forwarded to the given function. - See the OOP and FP equivalence table for a side-by-side comparison with the functional style, and the glossary for definitions of key terms.
Functional style
The functional style is based on function composition,
as demonstrated by the get_subscription_fee_by_email function
in the following example.
Example
>>> from typing import Literal
>>> from trcks import Result
>>> from trcks.fp.composition import pipe
>>> from trcks.fp.monads import result as r
>>>
>>> UserDoesNotExist = Literal["User does not exist"]
>>> UserDoesNotHaveASubscription = Literal["User does not have a subscription"]
>>> FailureDescription = UserDoesNotExist | UserDoesNotHaveASubscription
>>>
>>> def get_user_id(user_email: str) -> Result[UserDoesNotExist, int]:
... if user_email == "erika.mustermann@domain.org":
... return "success", 1
... if user_email == "john_doe@provider.com":
... return "success", 2
... return "failure", "User does not exist"
>>>
>>> def get_subscription_id(user_id: int) -> Result[UserDoesNotHaveASubscription, int]:
... if user_id == 1:
... return "success", 42
... return "failure", "User does not have a subscription"
>>>
>>> def get_subscription_fee(subscription_id: int) -> float:
... return subscription_id * 0.1
>>>
>>> def get_subscription_fee_by_email(user_email: str) -> Result[FailureDescription, float]:
... return pipe(
... user_email,
... get_user_id,
... r.map_success_to_result(get_subscription_id),
... r.map_success(get_subscription_fee),
... )
>>>
>>> get_subscription_fee_by_email("erika.mustermann@domain.org")
('success', 4.2)
>>> get_subscription_fee_by_email("john_doe@provider.com")
('failure', 'User does not have a subscription')
>>> get_subscription_fee_by_email("jane_doe@provider.com")
('failure', 'User does not exist')
Note
- The generic type
trcks.Resultallows domain errors to become part of a function's return type (subject to static type checking). - The modules
trcks.fp.compositionandtrcks.fp.monads.resultprovide a convenient way to chaintrcks.Result-returning functions and "regular" functions (in a type-safe way). - The
map*andtap*functions in the modules undertrcks.fp.monadsaccept extra positional and keyword arguments, which are forwarded to the given function. - See the OOP and FP equivalence table for a side-by-side comparison with the object-oriented style, and the glossary for definitions of key terms.
- If you type-check your code with
mypy,mypymay fail to infer the type arguments ofpipecalls that pass results of genericmap*ortap*helper functions. In this case, gather all arguments forpipein a variable annotated with an appropriatetrcks.fp.composition.Pipeline*type, and pass that variable topipewith unpacking (e.g.pipe(*pipeline)).
Still not convinced?
Explore these repositories for more ways to use trcks:
- trcks-example-cyclopts (CLI application)
- trcks-example-fastapi (REST backend application)
Or check the motivation
for railway-oriented programming and
for trcks.