I am learning a little functional programming and looking at toolz. The differences between compose, pipe, thread_first, and thread_last seem very subtle or non-existent to me. What is the intended different use cases for these functions?
compose
vs. thread_*
and pipe
compose
is a essentially a function compostion (∘). It's main goal is to combine different functions into reusable blocks. Order of applications is reversed compared to order of arguments so compose(f, g, h)(x)
is f(g(h(x)))
(same as (f ∘ g)(x) is f(g(x))).
thread_*
and pipe
are about using reusable blocks to create a single data flow. Execution can be deferred only with lazy operations, but blocks are fixed. Order of application is the same as order of arguments so pipe(x, f, g, h)
is h(g(f(x)))
.
compose
vs thread_*
.
compose
doesn't allow for additional arguments, while thread_*
does. Without currying compose
can be used only with unary functions.
Compared to that thread_
can be used with functions of higher arity, including commonly used higher order functions:
thread_last(
range(10),
(map, lambda x: x + 1),
(filter, lambda x: x % 2 == 0)
)
To the same thing with compose
you'd need currying:
pipe(
range(10),
lambda xs: map(lambda x: x + 1, xs),
lambda xs: filter(lambda x: x % 2 == 0, xs)
)
or
from toolz import curried
pipe(
range(10),
curried.map(lambda x: x + 1),
curried.filter(lambda x: x % 2 == 0)
)
thread_first
vs. thread_last
.
thread_first
puts piped argument at the first position for the function.
thread_last
puts piped argument at the last position for the function.
For example
>>> from operator import pow
>>> thread_last(3, (pow, 2)) # pow(2, 3)
8
>>> thread_first(3, (pow, 2)) # pow(3, 2)
9
In practice (ignoring some formalism) these functions are typically interchangeable, especially when combined with functools.partial
/ toolz.curry
and some lambda
expressions, but depending on the context, it is just more convenient to use one over another.
For example with built-in higher-order functions, like map
or functools.reduce
, thread_last
is a natural choice. If you want to reuse a piece of code in multiple place, it is better to use compose(h, g, f)
than adding function wrapper def fgh(x) pipe(x, f, g, h)
. And so on.