I love this example. I am training new Elixir users in  LiveView and OTP,
and there are a few burs that don't overly taint Elixir, but could smooth
the learning process. This is one of them. One of the techniques I teach is
naming concepts, and this is a great opportunity to name a concept.

Big +1 from me.

-bt

On Thu, Dec 16, 2021 at 2:50 PM Chris McCord <ch...@chrismccord.com> wrote:

> I was skeptical at first as well until I started thinking more about how
> convoluted some scenarios are today, and I think we on this mailing list
> especially are all likely a bit stuck in our ways. If you think about it
> from the beginner perspective, there is a ton of ceremony for certain
> operations, and some even today I have to go lookup to do basic things
> after almost 9 years of elixir. Consider the "make every other item
> highlighted" scenario. You either have to use `Enum.with_index` and
> `rem(index, 2) == 0`, which already is introducing two new APIs to the
> user, or you need to use Enum.zip with Stream.cycle:
>
>     <%= for {item, highlighted?} <- Enum.zip(items, Stream.cycle([true,
> false])) %>
>
> This looks reasonable, but is extremely non intuitive for newcomers, and
> even I have to think hard or lookup with I've done previously to remember
> it zip + cycle. Consider the alternative:
>
>     <%= for item <- items, let: {highlighted? = false} do
>       ...
>       <% highlighted? = not highlighted? %>
>     <% end %>
>
> This will also help cut down on the number of unnecessary traversals folks
> need to make in general, but performance is not close the main benefits
> here. While this at first felt like mutation to me and unlike anything we
> do in elixir, in my mind it actually aligns really well with the way
> variable rebinding already works – it is simply being extended to for
> bodies.
>
> +1 from me.
>
>
> On Dec 16, 2021, at 11:54 AM, Ben Wilson <benwilson...@gmail.com> wrote:
>
> I am with Louis and Paul so far I think. I won't repeat their comments but
> I think I can extend the issue by pointing out that this breaks refactoring
> for the inner contents of `for`. Previously, if you have:
>
> ```
>   for lesson <- section["lessons"], reduce: 0 do
>     counter ->
>       # complex multi-line-thing using the lesson and counter
>   end
> ```
>
> I can refactor this into:
>
> ```
>   for lesson <- section["lessons"], reduce: 0 do
>     counter ->
>       complex_operation(lesson, counter)
>   end
>
>   def complex_thing(lesson, counter) do
>     # complex multi-line-thing using the lesson and counter
>   end
> ```
>
> And everything just works, as is normal in Elixir code. The proposed
> changes would (as far as I can see) break this and that feels very
> unexpected and foreign.
>
> I sympathize with the problem space, but so far it's a -1 for me on this
> particular proposed improvement.
>
> - Ben
> On Thursday, December 16, 2021 at 10:02:49 AM UTC-5 José Valim wrote:
>
>> Note: This proposal contains images and rich text that may not display
>> correctly in your email. If so, you can read this proposal in a gist
>> <https://gist.github.com/josevalim/5c6735a4b90acc1bafdafec09acabe4f>.
>>
>> There is prior art in languages like Common Lisp, Haskell, and even in C#
>> with LINQ on having very powerful comprehensions as part of the language.
>> While Elixir comprehensions are already very expressive, allowing you to
>> map, filter, reduce, and collect over multiple enumerables at the same
>> time, it is still not capable of expressing other constructs, such as
>> map_reduce.
>>
>> The challenge here is how to continue adding more expressive power to
>> comprehensions without making the API feel massive. That's why, 7 years
>> after v1.0, only two new options have been added to comprehensions, :uniq
>>  and :reduce, to a total of 3 (:into, :uniq, and :reduce).
>> Imperative loops
>>
>> I have been on the record a couple times saying that, while many 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"
>> <https://github.com/josevalim/nested-data-structure-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, the most concise and clear solution is the Python
>> one, which I reproduce here:
>>
>> section_counter = 1lesson_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
>> <https://github.com/josevalim/nested-data-structure-traversal/blob/master/elixir/map_reduce.exs>.
>> I am pasting an image below which highlights certain aspects:
>>
>> [image: Screenshot 2021-12-13 at 10 02 48]
>> <https://user-images.githubusercontent.com/9582/145821890-6557ea21-e61f-4813-8c54-53c4ea1a9438.png>
>>
>>    -
>>
>>    Lack of reassignment: in Elixir, we can't reassign variables, we can
>>    only rebind them. The difference is, when you do var = some_value inside
>>    a if, for, etc, the value won't "leak" to the outer scope. This
>>    implies two things in the snippet above:
>>    1. We need to use Enum.map_reduce/3 and pass the state in and out
>>       (highlighted in red)
>>       2. When resetting the lesson counter, we need both sides of the
>>       conditional (hihhlighted in yellow)
>>    -
>>
>>    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 end in
>>    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 only reason I brought it up
>> is so we can confidently discard it from the discussion from now on. And
>> also because there is zero chance of the language suddenly becoming
>> whitespace sensitive.
>>
>> There is also zero chance of us introducing reassignment and making
>> mutability first class in Elixir too. The reason for this is because we all
>> agree that, the majority of the time, lack of reassignment and lack of
>> mutability are features that make our code more readable and understandable
>> in the long term. The snippet above is one of the few examples where we are
>> on the wrong end of the trade-offs.
>>
>> Therefore, how can we move forward?
>> 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).
>>
>> As mentioned in the introduction, we can express map, filter, reduce, and
>>  collect inside comprehensions. But how can we represent map_reduce in a
>> clear and concise way?
>> The :map_reduce option
>>
>> Since we have :reduce in comprehensions, we could introduce :map_reduce.
>> The solution above would look like this:
>>
>> {sections, _acc} =
>>   for section <- sections, map_reduce: {1, 1} do
>>     {section_counter, lesson_counter} ->
>>       lesson_counter = if section["reset_lesson_position"], do: 1, else: 
>> lesson_counter
>>
>>       {lessons, lesson_counter} =
>>         for lesson <- section["lessons"], map_reduce: lesson_counter do
>>           lesson_counter ->
>>             {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
>>
>> While there is a bit less noise compared to the original solution, the
>> reduction of noise mostly happened by the removal of modules names and a
>> few tokens, such as fn, (, and ). In terms of implementation, there is
>> still a lot of book keeping required to manage the variables. Can we do
>> better?
>> Introducing :let
>>
>> Our goal is to declare variables that are automatically looped within the
>> comprehension. So let's introduce a new option that does exactly that:
>> :let. :let expects one or a tuple of variables that will be reused
>> across the comprehension. At the end, :let returns a tuple with the
>> comprehension elements and the let variables.
>>
>> Here is how the solution would look like:
>>
>> section_counter = 1lesson_counter = 1
>> {sections, _} =
>>   for section <- sections,
>>       let: {section_counter, lesson_counter} do
>>     lesson_counter = if section["reset_lesson_position"], do: 1, else: 
>> lesson_counter
>>
>>     {lessons, lesson_counter} =
>>       for lesson <- section["lessons"], let: lesson_counter do
>>         lesson = Map.put(lesson, "position", lesson_counter)
>>         lesson_counter = lesson_counter + 1
>>         lesson
>>       end
>>
>>     section =
>>       section
>>       |> Map.put("lessons", lessons)
>>       |> Map.put("position", section_counter)
>>
>>     section_counter = section_counter + 1
>>     section
>>   end
>>
>> The :let option automatically takes care of passing the variables across
>> the comprehension, considerably cutting down the noise, without introducing
>> any mutability into the language. At the end, for+:let returns the
>> result of the comprehension plus the :letvariables wrapped in a tuple.
>> Extensions
>>
>> Here are some extensions to the proposal above. Not all of them might be
>> available on the initial implementation.
>> Let initialization
>>
>> You can also initialize the variables within let for convenience:
>>
>> {sections, _} =
>>   for section <- sections,
>>       let: {section_counter = 1, lesson_counter = 1} do
>>
>> This should be available in the initial implementation.
>> :reduce vs :let
>>
>> With :let, :reduce becomes somewhat redundant. For example,
>> Enum.group_by/2 could be written as:
>>
>> for {k, v} <- Enum.reverse(list), reduce: %{} do
>>   acc -> Map.update(acc, k, [v], &[v | &1])end
>>
>> with :let:
>>
>> {_, acc} =
>>   for {k, v} <- Enum.reverse(list), let: acc = %{} do
>>     acc = Map.update(acc, k, [v], &[v | &1])
>>   end
>>
>> The difference, however, is that :let returns the collection, while
>> :reduce does not. While the Elixir compiler could be smart enough to
>> optimize away building the collection in the :let case if we don't use
>> it, we may want to keep both :let and :reduceoptions for clarity. If
>> this is the case, I propose to align the syntaxes such that :reduce uses
>> the same semantics as :let. The only difference is the return type:
>>
>> for {k, v} <- Enum.reverse(list), reduce: acc = %{} do
>>   acc = Map.update(acc, k, [v], &[v | &1])end
>>
>> This can be done in a backwards compatible fashion.
>> after
>>
>> When you look at our solution to the problem using let, we had to
>> introduce temporary variables in order to update our let variables:
>>
>> {lessons, lesson_counter} =
>>   for lesson <- section["lessons"], let: lesson_counter do
>>     lesson = Map.put(lesson, "position", lesson_counter)
>>     lesson_counter = lesson_counter + 1
>>     lesson
>>   end
>>
>> One extension is to add after to the comprehensions, which are computed
>> after the result is returned:
>>
>> {lessons, lesson_counter} =
>>   for lesson <- section["lessons"], let: lesson_counter do
>>     Map.put(lesson, "position", lesson_counter)
>>   after
>>     lesson_counter = lesson_counter + 1
>>   end
>>
>> This does not need to be part of the initial implementation.
>> Summary
>>
>> Feedback on the proposal and extensions is welcome!
>>
>
> --
> 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-core+unsubscr...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/elixir-lang-core/22703d4b-60cb-4b0e-83d2-4a122f9147afn%40googlegroups.com
> <https://groups.google.com/d/msgid/elixir-lang-core/22703d4b-60cb-4b0e-83d2-4a122f9147afn%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>
>
> --
> 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-core+unsubscr...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/elixir-lang-core/657C7140-D8C2-4FF6-8616-3CD92400F1EB%40chrismccord.com
> <https://groups.google.com/d/msgid/elixir-lang-core/657C7140-D8C2-4FF6-8616-3CD92400F1EB%40chrismccord.com?utm_medium=email&utm_source=footer>
> .
>


-- 

Regards,
Bruce Tate
CEO

<https://bowtie.mailbutler.io/tracking/hit/f8218219-d2a8-4de4-9fef-1cdde6e723f6/c7c97460-016e-45fb-a4ab-0a70318c7b97>

Groxio, LLC.
512.799.9366
br...@grox.io
grox.io

-- 
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-core+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/elixir-lang-core/CAFXvW-5Wf57eXGxJR9b3VDkcg6uCE5nktTMYcttQtUa%2By%2BdB1A%40mail.gmail.com.

Reply via email to