Local accumulators for cleaner comprehensions

Yep, I am disagreeing with the need/desire for it to be more explicit. When there is a need for it, couldn’t a prefix be used as per my examples?

I’m happy with whatever José and the Elixir team and everyone else decide on tho, but just wanted to voice my support for the original which I quite liked :blush: (edit: and after seeing José’s block option and the reasons you and others have stated the block option is my favourite now).

The block solution is my preferred. Looks cleaner and it’s more obvious what is going on.

/chefs kiss

2 Likes

I liked the idea, but what about make it more general and call it let instead of accum, in a way that allows the same mechanism in a specific closed scope. I know that this resembles a closure mechanism, that would be useful in several ways, not only when using comprehensions.

EDIT:

There is an old proposal that seems to go in the same direction, but why not just make it more general, it would be useful and really explicit because any reference to that scoped variables outside a let block would be caught by Elixir compiler.

1 Like

Another option for a new syntax construct occurs to me, if (as this proposal seems to be uniquely focused on) you want to define local accumulators for any block. We could add to the family of special keywords do/end supports like else/catch/rescue/finally, say, acc.

This provides little value in the simplest if use-case, where you may as well use the else instead, but perhaps is nicer for complex cases and the for use-case.

else in with and catch in try/def already have precedence for accepting clauses and matching variables with ->. Perhaps we could accept clauses with the generator <- to indicate “generating” out into the external scope. So:

sum = 0

list =
  for element <- [1, 2, 3] do
    sum = element + sum
    element * 2
  acc
    sum <- sum
  end

list #=> [2, 4, 6]
sum #=> 6

This syntax says to me “after evaluating this block (however many times, each time for for) update the binding of the variable sum with the current value of sum, either before returning or iterating again.”

It allows for providing multiple reassignments with mulitple clauses, and doing other intermediate manipulations. Say,

i = 0

list = for element <- [1, 2, 3] do
    element * i
  acc
    i <- i + 1
  end

list #=> [0, 2, 6]
i #=> 3 (or 2?)

As others have pointed out before, while this functionality can be used to accumulate, especially if we’re adding it to other blocks than for, it really is just a rebinding tool. So perhaps rebind is a better keyword?

i = 0

list = for element <- [1, 2, 3] do
    element * i
  rebind
    i <- i + 1
  end

list #=> [0, 2, 6]
i #=> 3 (or 2?)
8 Likes

Under this construct, the original if example would become:

value = 123

if true do
  value = 456
rebind
  value <- value
end

value #=> 456

Not a verbosity or readability win, but a trivial case. With a more complicated conditional I can see resurfacing an intermediate value you don’t want to lose or return directly from the expression being useful.


The problem you are trying to address becomes:

section_counter = 0
lesson_counter = 0

for section <- sections do
  if section["reset_lesson_position"] do
    lesson_counter = 0
  rebind
    lesson_counter <- lesson_counter
  end

  section_counter = section_counter + 1

  lessons =
    for lesson <- section["lessons"] do
      lesson_counter = lesson_counter + 1
      Map.put(lesson, "position", lesson_counter)
    rebind
      lesson_counter <- lesson_counter
    end

  section
  |> Map.put("lessons", lessons)
  |> Map.put("position", section_counter)
rebind
  lesson_counter <- lesson_counter
  section_counter <- section_counter
end
2 Likes

Additionally, there is precedence for these do extensions not being allowed on just any block; for else is only allowed in if and with; catch in def and try; so we could pick and chose which block constructs this makes sense for (for and with being the obvious candidates).

1 Like

All the exemples that use a custom keyword to define a variable doesn’t really feels like the it is Elixir. But so far mut keyword felt the best, and we can declare and use Rust mut variables in a similar way, while temporaraly borrowring them to inner scopes.

But now I like this one the best. It feels like it fits very well with the current way Elixir looks and feels, while limiting the area of effect and introducing no additional keyword used to only to declare a variable.

But I feel like this solution might be too limiting, but also Elixir itself already have some similar limites that make a lot of sense.

2 Likes

Notice the question in the comment: i #=> 3 (or 2?)

It suggests two interesting questions about this implementation:

  1. Would the rebind run again after the final iteration?
  2. Would not re-referencing i after the for issue a unused variable warning?

The latter point seems compelling to me: if you’re not using the output rebinding, you probably shouldn’t be using the syntax at all! for Enum.with_index/for reduce: being a better alternative.

2 Likes

More idiomatically, I’d want to rewrite this as

section_counter = 0
lesson_counter = 0

for section <- sections do
  lesson_counter = if section["reset_lesson_position"], do: 0, else: lesson_counter

  lessons =
    for lesson <- section["lessons"] do
      Map.put(lesson, "position", lesson_counter)
    rebind
      lesson_counter <- lesson_counter + 1
    end

  section
  |> Map.put("lessons", lessons)
  |> Map.put("position", section_counter)
rebind
  lesson_counter <- lesson_counter
  section_counter <- section_counter + 1
end

But as this is effectively a do-while, it offsets the counts by 1 less. However we can’t initialize the starting values to 1 because then we’d appear to have counted 1 lesson if sections is empty. Not a perfectly elegant replacement, you’d have to do more special-casing before entering the loop.

The point about catch / rescue is a great one, seems like an implementation headache.

Accessing a local accumulator in these would need to be forbidden for this to work I suppose.

(Sorry just thinking aloud) Maybe a separate improvement to comprehensions could be a new break function which could be added and would allow the comprehension to bail. break could support these local accumulators and would allow things usually done with Enum.find_value & co

1 Like

I’ve definitely wanted break for my for compressions to replicate find and reduce_while. I know @hst337 knows that no one uses for compressions, but I use them all the time.

That’s the most pertinent question here and my answer is “no”. Making certain for scenarios more, ahem, comprehensible, to me is not worth it.

Your point that this does not introduce actual mutability and it still compiles to pure & immutable code is well-received and understood but I feel that many people will not see it that way and will start demanding more and more constructs they are familiar with.

Implementing this proposal carries a huge negative potential of “opening the flood-gates” kind.

And I can assure you I’ll be one of the first people to introduce a credo or ast-grep rule against using the new syntax in the code bases I’ll be responsible for.

IMO a good compromise would be to figure out how to extend for itself to allow for this and never allow this syntax to escape out of those borders.

Or, to put it another way: I don’t want only the runtime to enjoy pure and immutable code, I want I as the programmer to enjoy it too. :sweat_smile:

4 Likes

Indeed, write me down in that list.

This is very baffling, because I would understand introduction of real mutable values, just like haskell has and are used in a lower level development to get performance out of algorithms that require it, we accomplish this in elixir with NIFs at the end of the day.

1 Like

I am pretty sure that a proposal to introduce (more) mutable values would be considered even more baffling by most of the community. :sweat_smile: Which is likely an indicator of how divisive the topic is!

4 Likes

Apologies for second long comment, wanted to address what seems like the main contention points.

And that is a problem, because…? Every language and ecosystem has a learning curve and Elixir’s is still pretty mild. Many people out there have said so.

Who’s the target audience of this proposal?

The OP problem statement still seems like a toy problem so it’s hard for me to find empathy towards it.

I am trying hard to sympathize but the OP problem statement seems very niche, and the initial proposed solution looks like a confusing hill to die on.

YES, PLEASE! :pray:

Introduce whatever syntax monstrosity you like there! I personally will accept it immediately only after a small discussion on the syntax wording e.g. “accum” vs. “scoped_mutable” vs. “state” vs. whatever else gets the message across best.

You don’t have to do everything alone. We can, as a community, converge on a proposal that’s super difficult to implement and then make some sort of a contest who will implement it best. Why not?

1 Like

The whole reply you quoted that sentence from is meant to answer it. You may not agree with it but I am not going to rehash the same arguments. :slight_smile:

I will just plead for us to not get comfortable. All ecosystems have a learning curve. Ours may be mild. But it is not excuse for us to stop discussing ways to still improve it.

It came from an actual application. Seriously, please, let’s not get complacent and let’s not dismiss discussions by using labels such as “toy problems”. This is not ok.

Sure, the problem has been open for years, anyone can submit proposals! I welcome them. :smiley: As a matter of fact, I am not even comfortable with the current proposal but I keep submitting them to keep the ideas flowing. Which is why it gets very tiring when many comments are attempting to dismiss the problem in the first place.

9 Likes
accum session_counter <- 0, lesson_counter <- 0 do
  ...
end

I also had the same thought, either a special block or a special do alternative that supports reducing.

Perhaps reduce instead of accum?

reduce session_counter <- 0, lesson_counter <- 0 do
  ...
end

Notwithstanding the try catch problems, I like the functional concept of reducing over some arbitrary block of code which may use whatever is in scope.

Some further considerations

Is this an expression? Would it return a tuple of the values or would it just bind to what is provided?

Could we pipe into it for composition?

2 Likes

No working programmer will put any weight on new language syntax introduced just to make a yearly coding competition more convenient for themselves.

We have well-working constructs to solve that problem but you have clearly shown that you don’t want the legitimacy of the problem called into question, so OK.

For the moment: I am much more in favor of enriching for’s keyword list options or, more tentatively, the accumulators block.

Curious how will this proposal evolve.

The proposed new block construct is a nice idea. I like the special do:

However I find the following idea really interesting:

Now, if only we could somehow put i = 0 inside of the comprehension? In my humble opinion this has the potential to be the most elegant solution yet.

I know @josevalim has already said that comprehension syntax is crowded as it is and modifying it further would complicate it too much. But still, perhaps someone can come up with a way to make it work?

1 Like