3 ms·
I think that in every imperative language that offers `map`, `filter`, `reduce`, or similar, the written contract of this API should state that any higher-order
by Hackbraten 20d ago
I think that in every imperative language that offers `map`, `filter`, `reduce`, or similar, the written contract of this API should state that any higher-order function handed to it as an argument must be free from side effects.
I think I’ve seen several language core APIs have this in their contract, e.g. `Stream#reduce` in Java [0] (emphasis mine):
> accumulator - an *associative, non-interfering, stateless* function for combining two values
[0]: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-T-java.util.function.BinaryOperator- https://docs.oracle.com/javase/8/docs/api/java/util/stream/S...
- Someone 20d agoEven though it makes print debugging harder, I think it would be better if the language enforced such a contract.
- Skeime 20d agoI mean, most of the code that I write would be side-effect free anyway. In an imperative loop, this would also be true except for updating local variables. If this is the case, `reduce` really is the same as a loop over a collection, except that the names for the state passed between iterations come out better. In the `reduce` version, you can name the parameters to the reducer, but often not the return values. As a reader, one needs to connect the return values to the parameters by position. (Note that by "loop over a collection", I explicitly mean a looping construct that gives the elements of the collection directly, instead of looping over indices and extracting the elements manually.)
- speedstyle 20d agoIn Rust these specifically take `FnMut`, a function which can update internal/borrowed state, rather than `Fn` which can't easily. In `map` or `filter` you shouldn't rely on the iteration order so that's not often useful – maybe something 'logically' stateless but which needs a mutable connection/threadpool/cache, or eg a counter which is really an ancillary reduction. There's even `inspect` which is explicitly for such side effects. In `fold`, the order is guaranteed and you could use it for a state machine, a fiddly `zip` with other mutable iterators, etc – something you need to perform the reduction, but which isn't really an output, I think you could reasonably write either .fold(init, move |acc, x| {…}) // or .fold((state, init), |(state, acc), x| {…}).1
- KPGv2 17d ago> I think that in every imperative language that offers `map`, `filter`, `reduce`, or similar, the written contract of this API should state that any higher-order function handed to it as an argument must be free from side effects. Does the written contract for for-loops specify this as well? Because that's the obvious alternative for a reduce that an imperative programmer would reach for: a for-loop with the programmer managing the accumulation manually. I also don't see why a reduce should be side-effect free. There's nothing wrong with having a type def't for reduce be base.data.List.foldLeft : (b ->{e} a ->{e} b) -> b -> [a] ->{e} b Where {e} indicates a side effect. Here, the reducer can have side effects, and those side effects propagate to the result of the fold/reduce.