Is there no declarative / functional solution for "between" problems?

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.

using Base.Iterators
interpose(x, ys) = drop(flatten(zip(repeated(x), ys)), 1)
join(string.(1:10), "_") == join(interpose("_", string.(1:10)))

There’s IterTools.partition(iter, 2, 1) that iterates pairs of consecutive elements.

4 Likes