Skip to main content

Typesafe railway-oriented programming (ROP)

Project description

trcks 🚂

trcks is a Python library. It allows railway-oriented programming in two different programming styles:

  1. an object-oriented style based on method chaining and
  2. a functional style based on function composition.

Motivation

The following subsections motivate railway-oriented programming in general and the trcks library in particular.

Why should I use railway-oriented programming?

When writing modular Python code, return type annotations are extremely helpful. They help humans (and maybe LLMs) to understand the purpose of a function. And they allow static type checkers (e.g. mypy or pyright) to check whether functions fit together:

>>> def get_user_id(user_email: str) -> int:
...     if user_email == "erika.mustermann@domain.org":
...         return 1
...     if user_email == "john_doe@provider.com":
...         return 2
...     raise Exception("User does not exist")
...
>>> def get_subscription_id(user_id: int) -> int:
...     if user_id == 1:
...         return 42
...     raise Exception("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) -> float:
...     return get_subscription_fee(get_subscription_id(get_user_id(user_email)))
...
>>> get_subscription_fee_by_email("erika.mustermann@domain.org")
4.2

Unfortunately, conventional return type annotations do not always tell the full story:

>>> get_subscription_id(user_id=2)
Traceback (most recent call last):
    ...
Exception: User does not have a subscription

We can document domain exceptions in the docstring of the function:

>>> def get_subscription_id(user_id: int) -> int:
...     """Look up the subscription ID for a user.
...
...     Raises:
...         Exception: If the user does not have a subscription.
...     """
...     if user_id == 1:
...         return 42
...     raise Exception("User does not have a subscription")
...

While this helps humans (and maybe LLMs), static type checkers usually ignore docstrings. Moreover, it is difficult to document all domain exceptions in the docstring and to keep this documentation up-to-date. Therefore, we should use railway-oriented programming.

How can I use railway-oriented programming?

Instead of raising exceptions (and documenting this behavior in the docstring), we return a Result type:

>>> from typing import Literal
>>> from trcks import Result
>>>
>>> UserDoesNotHaveASubscription = Literal["User does not have a subscription"]
>>>
>>> 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"
...
>>> get_subscription_id(user_id=1)
('success', 42)
>>> get_subscription_id(user_id=2)
('failure', 'User does not have a subscription')

This return type

  1. describes the success case and the failure case and
  2. is verified by static type checkers.

What do I need for railway-oriented programming?

Combining Result-returning functions with other Result-returning functions or with "regular" functions can be cumbersome. Moreover, it can lead to repetitive code patterns:

>>> from typing import Union
>>>
>>> UserDoesNotExist = Literal["User does not exist"]
>>> FailureDescription = Union[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_fee_by_email(
...     user_email: str
... ) -> Result[FailureDescription, float]:
...     # Apply get_user_id:
...     user_id_result = get_user_id(user_email)
...     if user_id_result[0] == "failure":
...         return user_id_result
...     user_id = user_id_result[1]
...     # Apply get_subscription_id:
...     subscription_id_result = get_subscription_id(user_id)
...     if subscription_id_result[0] == "failure":
...         return subscription_id_result
...     subscription_id = subscription_id_result[1]
...     # Apply get_subscription_fee:
...     subscription_fee = get_subscription_fee(subscription_id)
...     # Return result:
...     return "success", 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')

Therefore, we need a library that helps us combine functions.

How does the module trcks.oop help with function combination?

The module trcks.oop supports combining functions in an object-oriented style using method chaining:

>>> from trcks.oop import Wrapper
>>>
>>> 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')

How does the package trcks.fp help with function combination?

The package trcks.fp supports combining functions in a functional style using function composition:

>>> from trcks.fp.composition import Pipeline3, pipe
>>> from trcks.fp.monads import result as r
>>>
>>> def get_subscription_fee_by_email(
...     user_email: str
... ) -> Result[FailureDescription, float]:
...     # If your static type checker cannot infer
...     # the type of the argument passed to `pipe`,
...     # explicit type assignment can help:
...     pipeline: Pipeline3[
...         str,
...         Result[UserDoesNotExist, int],
...         Result[FailureDescription, int],
...         Result[FailureDescription, float],
...     ] = (
...         user_email,
...         get_user_id,
...         r.map_success_to_result(get_subscription_id),
...         r.map_success(get_subscription_fee),
...     )
...     return pipe(pipeline)
...
>>> 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')

Setup

trcks is available on PyPI. Use your favorite package manager (e.g. pip, poetry or uv) to install it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

trcks-0.3.3.tar.gz (24.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

trcks-0.3.3-py3-none-any.whl (25.3 kB view details)

Uploaded Python 3

File details

Details for the file trcks-0.3.3.tar.gz.

File metadata

  • Download URL: trcks-0.3.3.tar.gz
  • Upload date:
  • Size: 24.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.8.6

File hashes

Hashes for trcks-0.3.3.tar.gz
Algorithm Hash digest
SHA256 7b453e60cf0ea735d9025edf321814d3e33e878b1a0b4b67311ddb1946d39573
MD5 b0e94acc2bc4b9d43cfde4f200968ac0
BLAKE2b-256 064769a11a0a686d9f4c32489df646aa24da3118b70909a4147b59aba2fe91b6

See more details on using hashes here.

File details

Details for the file trcks-0.3.3-py3-none-any.whl.

File metadata

  • Download URL: trcks-0.3.3-py3-none-any.whl
  • Upload date:
  • Size: 25.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.8.6

File hashes

Hashes for trcks-0.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 166f33738824a49c6fe6c0e7279b429721f78ca78b9fc49049f375ac0ae4ca60
MD5 43d63178fd92328c06d733f660035b45
BLAKE2b-256 4031c981973e2916993cdf79064c496d02af79c817105c274c8c8f99e96dae4c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page