I tried quite hard to find counter examples to this proposal, but after writing them I realize that they aren’t big deals or they are non issues.
With regular for you can first read the generators and filters, and then read the body of the comprehension. What you get in the body happens after all of the generators and filters were applied:
for x <- xs,
foo?(x),
bar?(x) do
# do something with x
end
With the introduction of let, I could argue that suddenly that’s no longer the case, the stuff we extract from the generators may change, or the filter turns into an exit condition, not a filter. This may lead to some confusion but then:
for let count = 0,
x <- xs,
foo?(x) and count <= 10 do
# do something with x and count
end
And it’s not hard to see what’s happening. What is confusing though, is that it’s not clear if this will cause all xs to be discarded because the filter returns false and the xs will be fully traversed, or it will lazily collect values from xs and abort the comprehension when the filter returns false.
Most of the examples shown here either bind a single variable, or use pattern matching with tuples. I could argue that it gets nasty very quickly with very long tuples and that it’s horrible having to figure out the position of a value in a tuple and where it binds to, but then you can do this:
{result, state} =
for let state = %{a: 1, b: :foo, c: []},
x <- xs do
# ...
state = %{state | c: [x + 1 | state.c]}
{x, state}
end
baz = state.c # ...
So again, not an issue.
So yeah, the only issue I see with the proposal is the lack of consensus over the “qualifier” name. The fact that let would be a function with special meaning to for is kind of alien to the way the rest of the language works, but when I try to imagine myself using it I don’t find it too much of an issue.
In any case, we can achieve the same today with map_reduce as you mentioned. Looking at the picture showing boilerplate vs what we actually care about:
And then again at the proposed code:
section_counter = 1
lesson_counter = 1
{sections, _acc} =
for let {section_counter, lesson_counter}, section <- sections do
lesson_counter = if section["reset_lesson_position"], do: 1, else: lesson_counter
{lessons, lesson_counter} =
for let lesson_counter, lesson <- section["lessons"] do
{Map.put(lesson, "position", lesson_counter), lesson_counter + 1}
end
section =
section
|> Map.put("lessons", lessons)
|> Map.put("position", section_counter)
{section, {section_counter + 1, lesson_counter}}
end
In terms of boilerplate reduction, I’m not sure there’s a substantial win. However, that aside, the benefits this proposal offers are, if I understand correctly:
- Allow to exit early from the comprehension by using the
letbindings in filters(so, sort of a reduce_while) - Allow “accumulator” or “state” value and binding to be defined in the same expression
I think 1 is the most important, as it enables more powerful patterns. I’m not entirely sure we are making a significant improvement in removing boilerplate, though. Honestly, I think it still looks ugly compared to python. The original proposal in the mailing list did address this issue, but it was too magical.
So in summary: the new syntax doesn’t feel wrong to me, and I’m interested in the new patterns it enables, but I’m not convinced that it reduces boilerplate, thus I’m not sure it would improve the situation for newcomers save for the “exit early” case.























