Skip to content

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
  1. The generic type trcks.Result allows domain errors to become part of a function's return type (subject to static type checking).
  2. The class trcks.oop.Wrapper provides a convenient way to chain trcks.Result-returning functions and "regular" functions (in a type-safe way).
  3. The map* and tap* methods of trcks.oop.Wrapper (and the other wrapper classes) accept extra positional and keyword arguments, which are forwarded to the given function.
  4. 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
  1. The generic type trcks.Result allows domain errors to become part of a function's return type (subject to static type checking).
  2. The modules trcks.fp.composition and trcks.fp.monads.result provide a convenient way to chain trcks.Result-returning functions and "regular" functions (in a type-safe way).
  3. The map* and tap* functions in the modules under trcks.fp.monads accept extra positional and keyword arguments, which are forwarded to the given function.
  4. 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.
  5. If you type-check your code with mypy, mypy may fail to infer the type arguments of pipe calls that pass results of generic map* or tap* helper functions. In this case, gather all arguments for pipe in a variable annotated with an appropriate trcks.fp.composition.Pipeline* type, and pass that variable to pipe with unpacking (e.g. pipe(*pipeline)).

Still not convinced?

Explore these repositories for more ways to use trcks:

  1. trcks-example-cyclopts (CLI application)
  2. trcks-example-fastapi (REST backend application)

Or check the motivation for railway-oriented programming and for trcks.