alux-traversable
traverse and the Traversable class are Conor McBride and Ross Paterson's, from Applicative programming with effects (Journal of Functional Programming 18(1):1–13, 2008, doi:10.1017/S0956796807006326) — the paper that introduced applicative functors and, with them, the operator for running an effectful function over a structure:
sequenceA = traverse id
We introduce Applicative functors — an abstract characterisation of an applicative style of effectful programming, weaker than Monads and hence more widespread.
… hence we introduce the type class Traversable, capturing functorial data structures through which we can thread an applicative computation.
— McBride and Paterson, §1 and §3
Jeremy Gibbons and Bruno C. d. S. Oliveira later showed in The essence of the Iterator pattern (Journal of Functional Programming 19(3–4):377–402, 2009, doi:10.1017/S0956796809007291) that this one operator is what the iterator pattern is reaching for: walking a structure, doing something effectful at each element, and rebuilding the structure with the effects sequenced.
t is the traversable structure and f is the applicative effect. Rust cannot quantify over t, so this crate provides the two instances worth having — Option and iterators — with f fixed to Result, whose applicative is the short-circuiting one. So: a function returning Result becomes composable inside an Option or an iterator, sequencing the Result effect while preserving the shape and order of the outer value. The suffix identifies the wrapper produced inside Result:
| Method | Mapping result | Meaning |
|---|---|---|
traverse |
Result<R, E> |
Maps each input to exactly one output. |
traverse_opt |
Result<Option<R>, E> |
Maps each input to zero or one output. |
traverse_iter |
Result<I, E> where I: IntoIterator |
Maps each input to zero or many outputs. |
sequence, sequence_opt, and sequence_iter are the corresponding identity mappings for values that already contain Result:
Some(42).traverse(|x| Ok(x + 1)) == Ok(Some(43))Some(Ok(42)).sequence() == Ok(Some(42))[1, 2].traverse(|x| Ok(x + 1)) == Ok(vec![2, 3])[1, 2].traverse_opt(|x| Ok(Some(x))) == Ok(vec![1, 2])[1, 2].traverse_iter(|x| Ok([x])) == Ok(vec![1, 2])[Ok(None), Ok(Some(1))].sequence_opt() == Ok(vec![1])
Which method applies is decided by what the mapping states inside Result, and what comes out is decided by what was traversed:
Option .traverse/sequence T ==> Option
Option .traverse/sequence Option ==> Option
Option .traverse/sequence Iter ==> Vec
Iter .traverse/sequence T ==> Vec
Iter .traverse/sequence Option ==> Vec
Iter .traverse/sequence Iter ==> Vec
Iterator traversal accepts stateful FnMut transformations, preserving source order and stopping at the first error.
The papers state the laws as naturality, identity, and composition: traversal commutes with applicative morphisms, traversal in the identity applicative is fmap, and two traversals composed are one traversal in the composed applicative. That last one is why the suffixes are worth having — traverse_opt and traverse_iter are each a traversal whose inner structure is already composed, so a caller writes one pass where two would nest.
Read in the Option and iterator shapes, the laws are:
- traversing
Noneperforms no effect and returnsOk(None); - iterator traversal preserves order and stops at the first error;
- optional iterator traversal omits
Nonewithout reordering the remaining values; - sequencing equals traversal by the identity function;
traverse_iterconcatenates each successful inner iterator in input order.
What it is for
Hold an Option<Key>, and a function that reads one: fn record(Key) -> Result<Record, Error>. Map one over the other and you get Option<Result<Record, Error>> — which is the two layers in the wrong order. ? works on the outside, and the failure is now on the inside, so it cannot be reached without taking the Option apart first. What the caller wants is Result<Option<Record>, Error>: fail if the read failed, state nothing if there was no key.
The paper arrives at it from the same direction, mapping a failure-prone function across a list and then collecting:
As you can see,
flakyMaptraversessstwice — once to applyf, and again to collect the results. More generally, it is preferable to define this applicative mapping operation directly, with a single traversal.— McBride and Paterson, §3
traverse is that single traversal: the swap done while mapping — one pass, and the result comes out with the failure outermost where ? can reach it:
| You hold | Mapping gives | traverse gives |
|---|---|---|
Option<A> and A -> Result<B, E> |
Option<Result<B, E>> |
Result<Option<B>, E> |
Vec<A> and A -> Result<B, E> |
Vec<Result<B, E>> |
Result<Vec<B>, E> |
Option<Result<A, E>> — already mapped |
Result<Option<A>, E>, by sequence |
The reason this is worth a name is that the layers accumulate. A store read both fails and states nothing: Result<Option<Raw>, Error>. What it states must still be decoded, which fails on its own: Raw -> Result<Record, Error>. Put the two together without traversing and you are holding Result<Option<Result<Record, Error>>, Error> before doing anything with it — and the next step adds another layer.
Why it reads better
Traversed, the path has one line per layer, and each line is an operation with a fixed meaning: traverse runs a fallible step inside a structure and brings the failure to the outside, traverse_opt does the same where the step itself may state nothing, traverse_iter where it may state several things. The shape of the code is the shape of the path, so a reader checks it by reading it, and a fourth level costs one more line rather than one more level of nesting.
By hand, every one of those layers becomes control flow invented on the spot: an early return here, an accumulator there, a continue that has to mean the right loop, a match whose two None arms mean different things. None of it is wrong Rust. What it lacks is that its correctness is re-established by reading it, at every site, every time it is touched — while the traversal's correctness was settled once, by the laws above, for every site at once. A loop states how; traverse states what, and that is the difference the combinator was named for.
The cost shows up in change, not in the first writing. Add a step to the traversed version and the types refuse anything that drops a layer. Add one to the hand-written version and the compiler is happy either way — a continue that should have been a return, or an accumulator cleared one loop too high, reads exactly like the code that was right.
Reading the example
Read it as one layer coming off at a time. raw(key)? leaves an Option<Raw>; traverse runs the decode inside it and states Result<Option<Record>, Error>, which ? opens again. traverse_opt on the parent key takes two layers at once — the key may not be there, and what it names may not be there either — and both come out as one Option<Record>. traverse over the parts turns many reads into one, stopping at the first key that had to be there. The hand-written twin meets the same three layers as a match returning early, a match inside a match whose two None arms mean different things, and a loop that has to remember which absence is fatal.
The example is a spec rather than an executable program: the store is stated over abstract types, nothing is implemented behind it, and nothing runs. That is the denotational style this workspace is written in, and it is also the shortest way to show a traversal without first inventing a database. That style is what the ALUX programming guidelines are about: Denotations and Laws and interpretations.
use ext;
use *;
/// Reads one whole assembly: a record, the record it may name, and the parts it does.
pub