Small unary transformations become more useful when their input and output contracts connect. Composition builds a larger transformation from those stages. This lesson calls its helper pipe and chooses left-to-right order: pipe(f, g)(x) computes g(f(x)). Other libraries use the name compose for right-to-left order, so always read the actual convention instead of guessing from a name.
def pipe(*stages):
def run(value):
for stage in stages:
value = stage(value)
return value
return run
count_words = pipe(str.strip, str.split, len)
print(count_words(" oak elm "))The intermediate value changes from a string to a list and finally an integer. Compatibility is about each adjacent contract, not one universal type for the whole pipeline. len can consume the list returned by split; str.upper cannot. More stages are not automatically a better design: name a meaningful pipeline and use a direct expression when it reads clearly.
Construction captures the stage functions without invoking them. Each later call starts with new input and a fresh local value. An empty pipe returns the exact input object, providing an identity transformation. This makes optional stages easy to assemble: an empty selection still produces a usable callable. Identity does not copy or freeze the object it returns.
Compatible pure stages can be grouped without changing the transformation: piping f, g and h gives the same final result as a nested pipe grouping f with g. Exchanging stages is a different matter. Adding two then multiplying by three differs from multiplying by three then adding two. Tests should use noncommuting operations because identical operations can conceal an ordering error.
A branching combinator, called juxt here, answers another question: what different views can I compute from the same input? It calls every branch on the original value and collects results in a tuple. The branch result does not feed the next branch.
def juxt(*functions):
return lambda value: tuple(function(value) for function in functions)
inspect_word = juxt(len, str.upper)
print(inspect_word("oak"))
print(juxt()("oak"))The branches execute sequentially from left to right. A fan-out drawing describes data dependencies, not threads or parallel execution. Each branch receives the same object; a mutating branch can therefore alter what later branches observe. This course assumes input-preserving policies for the functional model and tests the combinator's own evaluation order separately. It cannot prove arbitrary callbacks pure.
Exceptions stop the invocation immediately. A pipeline skips later stages, and juxt skips later branches. Work already done by an effectful callback is not rolled back. This simple failure policy is useful because callers do not need to interpret an undocumented mixture of partial output and swallowed errors.
For the build, verify stage order with arithmetic, type transitions with text, identity with object identity, and branching with two visibly different views. The fill focuses on deferred execution; the bug hunts distinguish reversing a pipe from accidentally threading one branch result into another.