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 # 42This 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 completeThis 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)