josevalim
Local accumulators for cleaner comprehensions
Hi everyone,
This is a proposal for introducing local accumulators to Elixir. This is another attempt of solving the comprehension problem, which we first discussed two years ago. This proposal is divided in four parts:
-
Problem statement
-
Comprehensions
-
Local accumulators (if you read the previous proposal, start from here!)
-
Revisiting the problem
-
Implementation considerations
Problem statement
I have been on the record a couple times saying that, while some problems are more cleanly solved with recursion, there is a category of problems that are much more elegant with imperative loops. One of those problems have been described in the “nested-data-structures-traversal” repository, with solutions available in many different languages. Please read the problem statement in said repository, as I will assume from now on that you are familiar with it.
Personally speaking, I find imperative solutions clearer and with considerably less boilerplate. The Python solution is the most concise one, which I reproduce here:
section_counter = 1
lesson_counter = 1
for section in sections:
if section["reset_lesson_position"]:
lesson_counter = 1
section["position"] = section_counter
section_counter += 1
for lesson in section["lessons"]:
lesson["position"] = lesson_counter
lesson_counter += 1
There are many things that make this solution clear:
- Reassignment
- Mutability
- Sensitive whitespace
Let’s compare it with the Elixir solution I wrote and personally prefer. I am pasting an image below which highlights certain aspects:

-
Lack of reassignment: in Elixir, we can’t reassign variables, we can only rebind them. The difference is, when you do
var = some_valueinside aif,for, etc, the value won’t “leak” to the outer scope. This implies two things in the snippet above:- We need to use
Enum.map_reduce/3and pass the state in and out (highlighted in red) - When resetting the lesson counter, we need both sides of the conditional (hihhlighted in yellow)
- We need to use
-
Lack of mutability: even though we set the lesson counter inside the inner
map_reduce, we still need to update the lesson inside the session (highlighted in green) -
Lack of sensitive whitespace: we have two additional lines with
endin them (highlighted in blue)
As you can see, do-end blocks add very litte noise to the final solution compared to sensitive whitespace. In fact, the reason why I brought it up is to make it clear they are not the source of verbosity, so we can confidentaly discard it from the discussion from now on. And also because there is zero chance of the language suddenly becoming whitespace sensitive. ![]()
However, there is still a gap to mind. So how can we move forward? Before we start on that, let’s learn what we can already do with comprehensions.
Comprehensions
Comprehensions in Elixir have always been a syntax sugar to more complex data-structure traversals. Do you want to have the cartesian product between all points in x and y? You could write this:
Enum.flat_map(x, fn i ->
Enum.map(y, fn j -> {i, j} end)
end)
Or with a comprehension:
for i <- x, j <- y, do: {i, j}
Or maybe you want to brute force your way into finding Pythagorean Triples?
Enum.flat_map(1..20, fn a ->
Enum.flat_map(1..20, fn b ->
1..20
|> Enum.filter(fn c -> a*a + b*b == c*c end)
|> Enum.map(fn c -> {a, b, c} end)
end)
end)
Or with a comprehension:
for a <- 1..20,
b <- 1..20,
c <- 1..20,
a*a + b*b == c*c,
do: {a, b, c}
There is no question the comprehensions are more concise and clearer, once you understand their basic syntax elements (which are, at this point, common to many languages). They support further options, such as :into, :uniq and :reduce.
Comprehensions start falling apart when you need to return additional values.
Let’s go back to our initial example. Imagine that you want to traverse a list of numbers, multiple each element in it by two while returning the sum of the original list at the same time.
In most non-functional programming languages, you might achieve this task like this:
sum = 0
list = []
for(element of [1, 2, 3]) {
list.append(element * 2)
sum += element
}
list /* [2, 4, 6] */
sum /* 6 */
This is quite different from how we have been doing things so far. In the example above, the for loop is changing the values of list and sum directly, which is then reflected in those variables once the loop is over.
Unfortunately, there is no mechanism to write this using for-comprehensions in Elixir. We need to fallback to map_reduce algorithms, going back to the problems described at the top of this post.
While you can use the
:reducein for-comprehensions to solve this problem, it has similar trade-offs to theEnum.map_reduce/3solution presented here.
Local accumulators
This proposal offers to introduce local accumulators to Elixir. This aims to improve the proposed solution by introducing local reassignment to the language. As mentioned earlier, variables defined or rebound inside an inner scope do not affect the outer scope in Elixir:
value = 123
if true do
value = 456
end
value #=> 123
With local accumulators, we can instead write:
@@value = 123
if true do
@@value = 456
end
@@value #=> 456
With local accumualtors we could solve the problem above like this:
@@sum = 0
list =
for element <- [1, 2, 3] do
@@sum = element + @@sum
element * 2
end
list #=> [2, 4, 6]
@@sum #=> 6
Local accumulators are local because can only be reassigned within their scope. You can mutate them inside if/unless, for comprehensions, cond, receive, case… but you can’t pass them to other functions, not even anonymous functions. Therefore, you cannot write this:
@@sum = 0
Enum.map([1, 2, 3], fn x ->
@@sum = x + @@sum
end)
In fact, the code above would fail to compile. Once we enter an anonymous function, any local accumulator becomes read-only. Passing a local accumulator to another function will only pass its current value.
In other words, the local accumulators are fully local and they never escape the current function, so their behaviour can never be observed outside of the function. Furthermore, Elixir will take care of compiling local accumulators to purely functional code!
Revisiting the problem
With all of this said, how we can solve the initial problem with local accumulators:
@@section_counter = 0
@@lesson_counter = 0
for section <- sections do
if section["reset_lesson_position"] do
@@lesson_counter = 0
end
@@section_counter = @@section_counter + 1
lessons =
for lesson <- section["lessons"] do
@@lesson_counter = @@lesson_counter + 1
Map.put(lesson, "position", @@lesson_counter)
end
section
|> Map.put("lessons", lessons)
|> Map.put("position", @@section_counter)
end
By adding reassignment to the language in the form of local accumulators, we can considerably reduce the amount of noise. We do this while still preserving the immutability semantics of data-structures and of all function callers. The only modification I have done to the solution is to start counting from zero, so we can store the incremented values in sessions and lessons.
I hope this provides some food for thought on this long-running discussion. Personally speaking, I find this solution gives the guarantees we expect from a functional language while providing a safe and clear alternative for those coming from imperative backgrounds.
Implementation considerations
This proposal uses the @@var notation to make it clear where local accumulators are used. @ is already used for module attributes, and there are similarties to them:
-
Module attributes are often used to accumulate values
-
When passing a module attribute or a local accumulators to a function, we simply pass the current value
However, they also have one large difference: module attributes are set using the @var :value notation, local accumulators use @@var = :value. When a module attribute is used inside a pattern, it is replaced by its contents. Local accumulators are reassigned.
There is also the possibility of confusion between them. Luckily, @@doc "foo" can raise a compilation error, which is a benefit of them having different write syntaxes. Furthermore, unused module attributes already warn and unused local accumulators shall warn too.
We should also consider the AST for local accumulators. @@var is already valid syntax today, and translates to {:@, _, [{:@, _, [{:var, _, nil}]}]}. Therefore, we could implement local accumulators today by pattern matching on the AST inside the implementation of module attributes. However, this has some downsides:
-
It will be easier to find and document if
@and@@are distinct operators -
@@will most likely need to be a special form, as it has special mearning by the compiler, and module attributes are not special forms today
Therefore, introducing @@ as a separate token and its own AST node is a backwards incompatible change to the language. We must assess the impact of such change (for example, by emitting warnings in the tokenizer before the feature is introduced and by parsing all packages in Hex for occurrences of @@).
Alternatives syntaxes
One possible alternative is to use an operator to assign to local accumulators, such as:
session_counter <~ session_counter + 1
Unfortunately the operator introduces a few issues:
-
There are different ways we can assign/bind to a variable in Elixir, such as the
=operator,caseclauses, on the left side of<-inwithclauses, etc -
Patterns may mix both “regular” variables and local accumulators in them and the operator does not allow us to express which is which
These reasons indicate local accumulators should be a property of the variable name.
Trending in Proposals
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #elixirconf-us
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security










First 10 of 245 Posts!
stevensonmt
I’ve gotten used to all the various workarounds to avoid mutability that I really don’t miss it with the current comprehensions in Elixir. That said this proposal looks really elegant and I’m sure I’ll abuse it when given the chance.
adw632
I think the term “accumulator” is a misnomer as the proposal says
@@don’t accumulate, they only bind.I don’t mind the concept, I just don’t like the association of
@with all of the module level use such module attributes and actual accumulation behaviour. I also find@@kind of visually jarring, ugly code.The following list might potentially confuse when used in a function and they look similar to their module counterparts:
It would also be problematic to blacklist the above as no doubt module level concerns may grow and change over time and there is a risk that some use of
@@vartoday will clash with some future module usage.If you are pinning one of these would the
^come before@@or after?^@@fooor@@^bar?I have to say that pinning with @@ makes for cryptic looking code.
Is there anything preventing another prefix such as
!foobeing used?Can
@@foobe used in function head pattern matching ?josevalim
I thought the same but I don’t think it is a concern in practice, Because they are set differently, you need to make several mistakes in a row. You have to write
@@doc = “my docs”, and even then, because you won’t read the local accumulator, you will still get an unused variable warnings.Other than that, you should be able to use
@@fooin a pattern and the syntax for pinning would be^@@foo.We can discuss other sigils instead of
@, but that’s pretty much venturing into personal taste directory.l3nz
I agree with you.
@is module, so kind-of the exact opposite of a local var.christhekeele
What do you think about:
@@var :initial_valueas a dedicated “declaration/instantiation” syntax@@var = :updated_valuebe the dedicated “update/reassignment” syntaxThe edge case I am thinking about is copying some block of code that declares and updates a
@@varvia@@var = :inner_initial,@@var = :inner_updateand pasting it into a larger block of code (for, say, some refactoring reason) that also declares and relies on an identically named@@var. Based on the proposal as defined so far, that inner copied@@var = :inner_initialwould unexpectedly start overriding the value in the outer scope.This is a shadowing issue not possible in purely functional, re-assignment-supported languages like today’s Elixir. Having a dedicated syntax for declaration vs update allows us to detect a shadowing issue in mutable variables. I think this is part of the reason why languages with flexible declaration + assignment rules and mutable variables have stylistically adopted
const/letdeclarations as best practice even when they are optional: it makes this sort of accidental shadowing much clearer by declaring the scope of a mutable var to start at a certain level of scoping, effectively re-initializing the accumulator to a known value in a nested scope and possibly issuing a compiler warning.These local accumulators don’t make sense without declaring an initial value, and having a separate syntax for declaration and update lets us catch shadowing similarly to how we do today with immutable variable references. It also harmonizes with
@var :intial_valuewhere it is clear that re-declaring the module attribute (with the exception of accumulating module attributes, which is a whole other confusing conflation of the terminology we are using here) will be overridden later in the scope.adw632
The new proposal is quite sound IMO and essentially just enabling rebinding within a scope, (which I support).
I would think it is not sufficiently alien to warrant a lot of controversy, and whats left is the asthetics.
So I do agree that it is personal taste to some degree, not discounting that anything starting with
@has to date been a module level concern.It is a shame we can’t reverse @@ to be module level and @ to be function level, but here we are.
adw632
I do wonder what behaviour will follow over the years. If people actually start using if/else/for constructs in anger with assignments in those blocks this will promote more use of @@ (and visually cryptic looking code).
Currently we think of @@ as the exception, but it doesn’t take too much imagination where “normal coding standards” may evolve over the years to recommending making top level variables within a function all rebindable by default (via @@) so that if/else/for “just work” by default, relegating local scope variables to something you only do in a local code block.
This is one reason why I asked can @@ be used in function head pattern matching or do you need a separate declaration/binding to get an
@@foo. Is there “ceremony” around declaring these vs other variables?Then eventually some will ask but why can’t you use @@ in a function head? Isn’t it just a variable with “better” rebindability? Why is this not the default?
christhekeele
One virtue of this proposal’s semantics is that local accumulators are only assignable to in function bodies, and module attributes are only assignable to in module bodies. That means that pretty much any implementation of either can co-exist with any syntax, including emitting deprecation warnings for an eventual swap in a breaking Elixir 2.0 release.
LostKobrakai
This to me makes this proposal a no-go. Elixir in its early days had this behaviour with normal variables and it was eventually removed to align with the idea of “everything is an expression” and using the return of the
if/else. Having the return value ofif/elsebe its result is imo an (/one of many) example of what makes elixir code easier to understand and follow than other languages, which do not enforce that explicitness.The fact that we now have to prefix variables with
@@imo doesn’t really change the fact that the whole snippet is now harder to understand. It might not look like it here, but imagine there being 20 more unrelated lines scattered between the lines shown here.I do like the efforts of trying to improve the lesson counting example, something which indeed benefits from improvement, but I’m not a fan of adding new features, which can easily cause code to be made worse in other places – other places which I expect to be larger in quantity than the improved ones.
I’ve answered many peoples questions around how “if/else” works in elixir, where they tried to go the assign within the block route and found it not working. By now I explained that this is not how things go and that they’d need to change their approach. Going forward I’d need to explain that things don’t work like that 'ish and that they could use this local accumulators thing, which would work the way they’ve been coding before, but is not really the thing they want to use, because it can easily make things harder to understand in many places, …. My expectation would be that the requirement to change is the larger effort than just prefixing a bunch of variables with
@@, so the latter will be what at least a bunch of beginners will be using.Instead of “the improvement when needed” it might become the default approach taken for them.
One other issue I see with the proposals around this topic (including previous proposals) is the focus on
for. I am a big fan offorand the conciseness it can provide. I also rarely use it – even though I’d love to do it more – because it’s so easy to introduce filtering of values, where filtering was not intended, but an error needed if a pattern couldn’t be matched for an element. In production code I’d rather be cautious and fail to process something instead of having elements be silently ignored on accident.I’d be fine with a solution only updating how
forworks. It’s limiting, but a tradeoff to be made just like the tradeoff between nestedEnumusage vs. a singlefor. But I’m not a fan of extending the changes toif/else, …, causing larger scale effects for all their use-cases, but excludingEnumAPIs, where people will still run into “problem statement”.josevalim
To clarify, @@ isn’t really function level. You can use it in any scope: inside an if branch, inside defmodule, or even at the root. Module attributes are definitely more common too, so I’d prefer the shorter one for them.
Last Post!
felix-starman
I’m for it
I absolutely would be having to teach new people to not abuse it, but honestly that’s what credo and the squint-test is for in my opinion.
I absolutely detested
withwhen it was added, and was convinced that combined with the changes toifscope binding it would result in horrible practices, that didn’t really happen, and I firmly feelwithwas the right choice now.Although I’ve seen some some nasty
with/elseclauses, the type of person writing those would have written something else equally hard to comprehend as they were learning, anyway.Request, or tooling that would make me feel even more comfortable
I like this proposal, my only ask would be to consider an easy way for the compiler to emit metadata if the caller of a function passes a locally accumulated variable to a function where that function binds a local accumulator of the same name in its function head.
In the example below, we can imagine someone new to Elixir not quite understanding local accumulators, the same way sometimes people don’t understand immutability right away. We could imagine a scenario where they think they are properly decrementing the allowed number of holds inside
check_out/2, but that gets thrown away.Looking at it as someone with years of Elixir experience it might be easier to “spot”, but for someone new they wouldn’t necessarily catch it.
It would be nice to have a way for a new-to-Elixir developer to realize “Oh, the compiler says I used a local accumulator but didn’t rely on syntax that I couldn’t have just done with a regular variable, and it’s ‘shadowing the same variable name in a caller’. OOOOooooh, THAT’s why my code is allowing unlimited books/dvds from the library!”
The compiler probably shouldn’t emit that as a warning for binding a local accumulator in the function head, because there’s plenty of reasons why you might reuse the same variable name everywhere (e.g. token based patterns, like
Plug.Conn.t()),but I would want the ability to quickly use compiler tracing to determine where a local accumulator is used, where it’s passed, and where the same name is reused in that dependency chain.
Credo can’t really do that, since it’s been around since before compiler tracing (I really want something as robust as credo, but entirely tracing based), but I feel like that’s the kind of “helping hand” I’d love the compiler to either provide, or easily enable.
I guess I’m saying I like Rust’s compiler warnings about
"variable does not need to be mutable"formut var