I was wondering what a more declarative approach is to the simple problem space of doing something between the elements of some iterator. I feel like that is a task that constantly comes up but can’t be solved with just map
or zip
and friends, because the length of the iterator is of course one more than the iterator of “betweens”.
One example is getting all the midpoints of a range of values. Or checking that some condition holds between subsequent values in a vector. Or intertwining a transformation of the iterator with some values in between. One good example that this is not completely trivial is the implementation of join
:
function join(io::IO, iterator, delim, last)
first = true
local prev
for item in iterator
if @isdefined prev
first ? (first = false) : print(io, delim)
print(io, prev)
end
prev = item
end
if @isdefined prev
first || print(io, last)
print(io, prev)
end
nothing
end
You can see here that the quite rarely seen @isdefined
macro is used to work around the off-by-one annoyance, that no prev
exists on the first run. Generally I always see solutions like checking “am I still in the first run?” or “am I in the last run, yet?” leading to frequent bugs and hard-to-read code.
I would like to see nice generic higher-order functions that can be used to implement join
, and midpoints
and other things like that, both eager and collecting like map
or lazy like Iterators.map
.
And these functions should work with stateful iterators where you can’t get the same element twice and can’t index, because one common solution is map(f, @view(vec[1:end-1]), @view(vec[2:end]))
and I don’t like that one either.
Maybe all my use cases can be thought of in terms of reducers, but I’m not quite clear how these should be set up to be generic and performant.