better-py

Writer Monad

Log computations and accumulate values

The Writer monad represents computations that produce a value along with a log or accumulator. It's useful for logging, tracking, and accumulating values through a chain of operations without side effects.

Writer[W, A] contains a value of type A and a log of type W. The log type must be a monoid (supporting combination and identity), including built-in types like list, str, int, float, and set.

Creating Writers

Wrapping Values with Logs

The Writer constructor creates a writer with a value and an accompanying log.

from better_py import Writer

# Create with list log
writer = Writer(["log1", "log2"], 42)
log, value = writer.tell()
log    # ["log1", "log2"]
value  # 42

# Create with string log
writer = Writer("Step 1: ", "result")
log, value = writer.tell()
log    # "Step 1: "
value  # "result"

Use the constructor when you need to create a writer with both a value and its log.

Creating Typed Writers

Convenience functions create writers for common monoid types.

from better_py import list_writer, str_writer, sum_writer

# List writer (concatenation)
list_writer([1, 2], "a").tell()  # ([1, 2], "a")

# String writer (concatenation)
str_writer("hello ", "world").tell()  # ("hello ", "world")

# Sum writer (addition)
sum_writer(5, "result").tell()  # (5, "result")

Use these helpers when working with specific monoid types for cleaner code.

Logging Without Values

The Writer.tell_log method creates a writer with only a log entry and no value.

from better_py import Writer

Writer.tell_log(["debug message"]).tell()  # (["debug message"], None)

Use this when you need to log something without producing a meaningful value.

Extracting Values

Getting Both Log and Value

The tell method returns both the log and the value as a tuple.

from better_py import Writer

writer = Writer(["log1", "log2"], 42)
log, value = writer.tell()
log    # ["log1", "log2"]
value  # 42

This is the primary way to extract results from a Writer computation.

Transforming Writers

Mapping Values

The map method transforms the value while preserving the log.

from better_py import Writer

writer = Writer(["log"], 5)
mapped = writer.map(lambda x: x * 2)
mapped.tell()  # (["log"], 10)

Use map when you need to transform the computation result without touching the log.

Chaining Operations

The flat_map method chains Writer operations, automatically accumulating logs.

from better_py import Writer

def step1(x):
    return Writer(["step1"], x + 1)

def step2(x):
    return Writer(["step2"], x * 2)

writer = Writer(["init"], 5)
result = writer.flat_map(step1).flat_map(step2)
result.tell()  # (["init", "step1", "step2"], 12)

Use flat_map for sequential operations where each step produces a new log entry.

Advanced Operations

Capturing Log and Value

The listen method transforms a Writer returning the log alongside the value.

from better_py import Writer

writer = Writer(["log"], 42)
listened = writer.listen()
listened.tell()  # ((["log"], 42), ["log"])

Use this when you need to pass both the value and its log to subsequent operations.

Passing Log Through

The pass_ method replaces the value with the log.

from better_py import Writer

writer = Writer(["log"], 42)
passed = writer.pass_()
passed.tell()  # (["log"], ["log"])

Use this when you want to propagate the log as the value itself.

Real-World Pattern: Audit Trail

from better_py import str_writer

def transfer_money(from_account: int, to_account: int, amount: float):
    return (str_writer(f"Validating transfer of ${amount} from {from_account} to {to_account}\n", None)
        .map(lambda _: validate_accounts(from_account, to_account))
        .flat_map(lambda _: str_writer(f"Transfer validated\n", amount))
        .flat_map(lambda _: str_writer(f"Executing transfer\n", execute_transfer(from_account, to_account, amount)))
        .map(lambda _: str_writer(f"Transfer complete\n", None)))

result = transfer_money(1, 2, 100.0)
log, _ = result.tell()
# log:
# Validating transfer of $100.0 from 1 to 2
# Transfer validated
# Executing transfer
# Transfer complete

This pattern shows Writer's strength for audit trails: every operation logs its action, creating a complete record of the computation while keeping the business logic pure.

Custom Monoids

You can use custom types as logs by implementing the Monoid protocol with combine and identity methods.

from better_py import Writer, Monoid
from dataclasses import dataclass

@dataclass
class LogEntry:
    level: str
    message: str

    def combine(self, other: "LogEntry") -> "LogEntry":
        return LogEntry(
            level=self.level,
            message=f"{self.message}; {other.message}"
        )

    @staticmethod
    def identity() -> "LogEntry":
        return LogEntry(level="INFO", message="")

# Use with Writer
writer = Writer(LogEntry("INFO", "Step 1"), "result")

Custom monoids let you create domain-specific logging types that accumulate according to your business rules.

When to Use

Use Writer when:

  • You need to log computations
  • You want to accumulate values
  • You need audit trails
  • You're building debug/trace systems
  • You want to track computation steps
  • You need pure functional logging

Don't use Writer when:

  • You only need the result (use plain functions)
  • Logging is handled externally
  • You need different log types per operation
  • Side effects are acceptable (use logging libraries instead)

See Also

  • State - For state threading
  • IO - For side effect management
  • Reader - For dependency injection

On this page