Build a Data Processing Pipeline with Functors

In this tutorial, we will build a data processing pipeline that transforms values inside containers. Along the way, we will encounter fmap on Maybe and ImmutableList, and learn how transformations compose cleanly when data may be missing.

Prerequisites

Step 1: Transform a Single Optional Value

Create a new Python file called pipeline.py with the following contents:

from katharos.types import Maybe

age = Maybe[int].Just(25)
doubled = age.fmap(lambda x: x * 2)
print(doubled)

age = Maybe[int].Nothing()
doubled = age.fmap(lambda x: x * 2)
print(doubled)

Run the file:

python pipeline.py

You should see:

Just(50)
Nothing()

Notice that the value was doubled in the first case. In the second case the function was not called at all — there was no value to transform.

Step 2: Process User Ages

Now we will build a function that looks up user ages. Replace the contents of pipeline.py with:

from katharos.types import Maybe

def get_user_age(user_id: int) -> Maybe[int]:
    users = {1: 25, 2: 30, 3: 35}
    age = users.get(user_id)
    if age is None:
        return Maybe[int].Nothing()
    return Maybe[int].Just(age)

current_year = 2026
birth_year = get_user_age(1).fmap(lambda age: current_year - age)
print(birth_year)

birth_year = get_user_age(999).fmap(lambda age: current_year - age)
print(birth_year)

Run the file:

python pipeline.py

You should see:

Just(2001)
Nothing()

Notice how the birth year calculation is skipped entirely when the user does not exist.

Step 3: Chain Multiple Transformations

We will now chain multiple transformations together. Add this code at the bottom of pipeline.py:

result = (
    get_user_age(2)
    .fmap(lambda age: age + 5)
    .fmap(lambda age: age * 2)
    .fmap(lambda age: f"Age in dog years: {age}")
)
print(result)

result = (
    get_user_age(999)
    .fmap(lambda age: age + 5)
    .fmap(lambda age: age * 2)
    .fmap(lambda age: f"Age in dog years: {age}")
)
print(result)

Run the file:

python pipeline.py

You should see the new lines at the bottom of the output:

Just('Age in dog years: 70')
Nothing()

Notice that all three transformations were skipped for the missing user. Each transformation preserves the Nothing() without any extra checking.

Step 4: Transform Multiple Values in a List

Now we will transform all values in an ImmutableList. Add this code at the bottom of pipeline.py:

from katharos.types import ImmutableList

ages = ImmutableList([20, 25, 30, 35, 40])
doubled = ages.fmap(lambda x: x * 2)
print(doubled)

Run the file. You should see a new line at the bottom:

ImmutableList([40, 50, 60, 70, 80])

Notice that fmap applied the function to every element, producing a new list.

Step 5: Process a List of User IDs

We will now process multiple user IDs at once. Add this code at the bottom of pipeline.py:

user_ids = ImmutableList([1, 2, 3])
current_year = 2026
birth_years = user_ids.fmap(
    lambda user_id: get_user_age(user_id).fmap(lambda age: current_year - age)
)
print(birth_years)

Run the file. You should see:

ImmutableList([Just(2001), Just(1996), Just(1991)])

Notice how fmap composes: the outer fmap iterates over the list, and the inner fmap transforms each Maybe age into a birth year.

Step 6: Build a Complete Processing Pipeline

Now we will combine everything into a reusable pipeline function. Add this code at the bottom of pipeline.py:

def process_user(user_id: int) -> Maybe[str]:
    current_year = 2026
    return (
        get_user_age(user_id)
        .fmap(lambda age: current_year - age)
        .fmap(lambda birth_year: f"Born in {birth_year}")
    )

def format_result(maybe_message: Maybe[str]) -> str:
    if maybe_message.is_just():
        return maybe_message.value
    return "User not found"

user_ids = ImmutableList([1, 2, 3, 999])
results = user_ids.fmap(process_user)
formatted = results.fmap(format_result)

for message in formatted:
    print(message)

Run the file. The final lines of the output should be:

Born in 2001
Born in 1996
Born in 1991
User not found

Notice that the missing user (ID 999) produced the fallback string "User not found" — the Maybe propagated through the whole pipeline and was only converted to a plain value at the very end.

What We Built

We built a data processing pipeline that transforms values inside Maybe and ImmutableList containers using fmap. The same operator works on both types, and transformations compose cleanly without any explicit None checks.