The generic "walk a collection, threading an accumulator through a
fallible step, stopping at the first failure" primitive -- extracted
from a pattern independently hand-rolled at least ten times across
Ichor's own compiler internals before this existed: build_ruleset/1
(duplicated near-verbatim across Ichor.EBNF.ISO.Actions,
Ichor.EBNF.W3C.Actions, Ichor.BNF.Actions, Ichor.ABNF.Actions, and
Ichor.PEG.Actions), Ichor.Backtrack.Bindings' own unify_compound/5
inner reduce over zipped compound-term arguments (whose failure
sentinel is a bare :fail, not {:error, _}), Aether.Eval's own
process_defs/2, and -- byte-for-byte identical to each other --
Ichor.Actions' own eval_one/2 list-of-captures branch, and
Aether.Eval's own convert_list/3 and convert_seq_terms/3.
Elixir's stdlib has no Enum/Result-traverse equivalent to lean on
here (unlike, say, Haskell's Traversable or Rust's
Iterator::collect::<Result<_, _>>), which is exactly why this ended
up hand-rolled so many times.
Both functions pass any non-{:ok, _} step result straight through,
unexamined -- deliberately, so the toolkit never has an opinion on
what a "failure" looks like: it works equally for a bare :fail atom
(Bindings.unify_compound/5's convention) and for {:error, reason}
(every other call site's convention).
Summary
Functions
Maps step over list in order, collecting each value it produces
while threading state through sequentially, stopping at the first
failure -- exactly the shape eval_one/2's list branch, convert_list/3,
and convert_seq_terms/3 each independently wrote (build the result
via [v | acc], reverse once at the end, thread state, halt on
failure). Built on reduce_ok/2 rather than duplicating its own
Enum.reduce_while.
Like Enum.reduce/3, but step returns {:ok, new_acc} to continue
or anything else to stop immediately and return that value instead --
the "reduce with early exit on failure" shape build_ruleset/1,
unify_compound/5, and process_defs/2 each wrote their own copy of.
Functions
@spec map_ok([elem], state, (elem, state -> {:ok, value, state} | failure)) :: {:ok, [value], state} | failure when elem: term(), state: term(), value: term(), failure: term()
Maps step over list in order, collecting each value it produces
while threading state through sequentially, stopping at the first
failure -- exactly the shape eval_one/2's list branch, convert_list/3,
and convert_seq_terms/3 each independently wrote (build the result
via [v | acc], reverse once at the end, thread state, halt on
failure). Built on reduce_ok/2 rather than duplicating its own
Enum.reduce_while.
@spec reduce_ok(Enumerable.t(elem), acc, (elem, acc -> {:ok, acc} | failure)) :: {:ok, acc} | failure when elem: term(), acc: term(), failure: term()
Like Enum.reduce/3, but step returns {:ok, new_acc} to continue
or anything else to stop immediately and return that value instead --
the "reduce with early exit on failure" shape build_ruleset/1,
unify_compound/5, and process_defs/2 each wrote their own copy of.