Enum.fold/2 and Enum.fold/3

66 views
Skip to first unread message

Bruce Tate

unread,
Dec 9, 2020, 9:40:52 AM12/9/20
to elixir-l...@googlegroups.com
This is a long post, and I think it might not be a popular one. Still, I think it has an important punchline, so bear with me.

Proposal: Add Enum.fold/2 and Enum.fold/3 (and the Stream counterparts). These both work like Enum.reduce, but flip the arguments in the reducer function. 

As I teach and code Elixir, I have four steps for most students. First, we build common vocabulary and language constructs of Elixir. Next, we think in terms of a common pattern I call constructor-reducer-converter. (More in a sec.) Then, we apply that pattern in higher level constructs like LiveView and OTP. Finally, we build projects and let students code these patterns for themselves. In a phrase, *everything is reduce*. I want to focus on that second step. 

I am thinking and teaching more about one common pattern. At its core, Elixir modules have functions around a common type T that either produce, transform, or consume T. I call these functions constructors, reducers, and converters. They all interact with type T. 

Think of them in this way: 

constructor(inputs) |> reducer(...) |> reducer(...) ... |> converter

This is the way the data-centric modules in Elixir are built today. In Designing Elixir Systems with OTP speak, they are the *core* modules, and I believe many Elixir developers build code this way, even if we don't vocalize it in this way. 

Building code like this puts us in conflict in one important way:  

1. Elixir modules are generally organized around data of a common type (let's say T)
2. Named functions in a module have T as the first argument. 
3. Pipes rely on T as a first argument. 
4. In Enum.reduce, T is the accumulator, and it's the second argument of the reducer. 

3 and 4 are in conflict! The accumulator plays the role of T in the reducer: 

reducer(any(), T) :: T

That's backwards, and it won't pipe. 

If we have this: 

10 |> subtract(4) |> subtract(3) 

and we want to express that code over a list that's arbitrarily long, we we must do this:

Enum.reduce([4, 3, ...], 10, fn x, acc -> subtract(acc, x) end)

to flip the two arguments to the correct place. 

But we can't do so. I know the Elixir way is to generally put up with a little ceremony for the common good of a tighter standard library, most of the time, but I don't think this problem applies. I think that Elixir *wants* this function: 

Enum.fold([4, 3], 10, &subtract2)

To me the cost is high: 

1. We must add a function to Enum, a central Elixir function
2. The function does almost the same thing as an existing function. 

To me at least the benefit is higher: 

Enum.fold encourages us to write better code in three ways: 

A, Enum.fold encourages us to write code with T first, the Elixir way. 

B, Enum.fold encourages named functions rather than anonymous functions which leads to more opportunities to name concepts without comments, always a good thing. 

C. This strategy has a profound impact on tests of reducers, as we can simply pipe them and check the results. 

What do you think?
-bt








--

Regards,
Bruce Tate
CEO

José Valim

unread,
Dec 9, 2020, 10:08:07 AM12/9/20
to elixir-l...@googlegroups.com

Thanks Bruce for the proposal.

I have run into this and, generally speaking, I have addressed it by using &1, ...:

Enum.reduce([4, 3, ...], 10, &subtract(&2, &1))

Still quite concise. Most often the function I call in reduce also needs other parameters and that handles it well:

Enum.reduce([4, 3, ...], 10, &something(&2, param, &1))

My concern with adding fold is that it will spiral out of control: fold, map_fold, flat_map_fold, fold_while, etc.

Bruce Tate

unread,
Dec 9, 2020, 10:24:17 AM12/9/20
to elixir-l...@googlegroups.com
Good points. 

The form Enum.reduce([4, 3, ...], 10, &subtract(&2, &1)) is pretty reasonable. 

-bt

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4KUbSv0diJvjcOCM55jfUiP%2Bi4-rshT6zCyQk_cWAu8LA%40mail.gmail.com.

Allen Madsen

unread,
Dec 9, 2020, 12:27:56 PM12/9/20
to elixir-l...@googlegroups.com
FWIW, I've always appreciated that reduce passes them in the order it does to the function, because it's the same order that they are passed into reduce, which has made it easy for me to remember the order they come in.

Enum.reduce(items, acc, fn item, acc -> ... end)

eksperimental

unread,
Dec 9, 2020, 3:10:35 PM12/9/20
to elixir-l...@googlegroups.com
Looking back, I think having acc as the first argument would be have a
better choice. But we have other functions named after "fold" such as
List.foldl/3, List.foldr/3, Inspect.Algebra.fold_doc/2 that take a
function in the same format as Enum.reduce/3. So having some
fold functions that take a function in the (current_acc, element ->
updated_acc) format and others in the (element, current_acc ->
updated_acc) would be hard to remember which one is which. I guess this
proposal should consider a different name for the new function.

Bruce Tate

unread,
Dec 9, 2020, 6:28:50 PM12/9/20
to elixir-l...@googlegroups.com
Withdrawn. José convinced me. 

-bt

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages