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?
Use cases describe the functional requirements of a system from the end user's perspective, creating a goal-focused sequence of events that is easy for users and developers to follow.
Difference between scenario and use cases- A scenario is a situation in which more than one actor is involved in performing a specific task. A use case is the description of the scenario which involves describing the functionality of the scenario.
The most comprehensive technique for identifying use cases is the event decomposition technique. The event decomposition technique begins by identifying all the business events that will cause the information system to respond, and each event leads to a use case.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With