Sequencing Side Effects with IO

In this tutorial, we will build a data processing script that defers all of its console output until the very end of the computation. Along the way, we will encounter IO, FunctionWithSideEffect, fmap, and the >> operator for sequencing side effects.

Prerequisites

Step 1: Create a Pure IO Value

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

from katharos.types.side_effect import IO

score = IO(0)
print(score.value)
score.execute()
print("execute() returned")

Run the file:

python audit.py

You should see:

0
execute() returned

Notice that execute() ran without printing anything. A plain IO(value) holds a value but has no side effect attached to it.

Step 2: Transform the Value with fmap

Now we will apply a transformation to the value inside IO. Replace the contents of audit.py with:

from katharos.types.side_effect import IO

score = IO(10)
doubled = score.fmap(lambda x: x * 2)
print(doubled.value)

Run the file:

python audit.py

You should see:

20

Notice that fmap transforms the value and returns a new IO. No side effects are involved.

Step 3: Attach a Side Effect

Now we will create an IO that also prints a log message when executed. Replace the contents of audit.py with:

from katharos.types.side_effect import IO, FunctionWithSideEffect

score = IO(
    10,
    FunctionWithSideEffect(f=lambda: print("Score recorded: 10")),
)

print(score.value)
print("About to execute...")
score.execute()

Run the file:

python audit.py

You should see:

10
About to execute...
Score recorded: 10

Notice that the print statement only appeared after execute() was called. The FunctionWithSideEffect stores the action; it does not run it immediately.

Step 4: Sequence Two IO Actions with >>

Now we will chain two IO actions together so both of their side effects run in order. Replace the contents of audit.py with:

from katharos.types.side_effect import IO, FunctionWithSideEffect

step1 = IO(
    10,
    FunctionWithSideEffect(f=lambda: print("Step 1: initial score = 10")),
)
step2 = IO(
    20,
    FunctionWithSideEffect(f=lambda: print("Step 2: doubled score = 20")),
)

pipeline = step1 >> step2

print(f"Final value: {pipeline.value}")
print("Running side effects:")
pipeline.execute()

Run the file:

python audit.py

You should see:

Final value: 20
Running side effects:
Step 1: initial score = 10
Step 2: doubled score = 20

Notice two things:

  • pipeline.value is 20 — the value from the second IO. The >> operator keeps the value of the right-hand side and discards the value of the left-hand side.

  • Both side effects ran in order when execute() was called, even though step1’s value was discarded.

Step 5: Build a Complete Audit Pipeline

Now we will build a three-step pipeline that logs each processing step and executes the entire audit log at the end. Replace the contents of audit.py with:

from katharos.types.side_effect import IO, FunctionWithSideEffect

initial = 10

step1 = IO(
    initial,
    FunctionWithSideEffect(f=lambda: print(f"[AUDIT] Received score: {initial}")),
)

doubled = initial * 2
step2 = IO(
    doubled,
    FunctionWithSideEffect(f=lambda: print(f"[AUDIT] Doubled score: {doubled}")),
)

bonus = doubled + 5
step3 = IO(
    bonus,
    FunctionWithSideEffect(f=lambda: print(f"[AUDIT] Added bonus: {bonus}")),
)

pipeline = step1 >> step2 >> step3

print(f"Final score: {pipeline.value}")
print("--- Audit log ---")
pipeline.execute()

Run the file:

python audit.py

You should see:

Final score: 25
--- Audit log ---
[AUDIT] Received score: 10
[AUDIT] Doubled score: 20
[AUDIT] Added bonus: 25

Notice that all three audit entries appeared together under the “Audit log” heading, not scattered across the computation. The >> operator accumulated all three side effects into a single IO that we executed once at the end.

If your audit log is missing entries, you have probably forgotten to connect a step with >> or forgotten to call execute().

What We Built

We built a data processing script where all side effects are deferred and collected into a single IO using >>. Calling execute() once at the end runs every accumulated side effect in the order they were chained. The computation itself (values) and the observable output (side effects) are kept completely separate.