How about this: for section <- sections, let: section_counter <- 1, let: lesson_counter <- 1 do
lesson_counter = if section["reset_lesson_position"], do: 1, else: lesson_counter {lessons, lesson_counter} = for lesson <- section["lessons"], let: lesson_counter <- lesson_counter 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 Rationale: - "section <- sections" already rebinds the section variable on each loop, so feels like a natural way to signal that something is changing on each iteration, - "let: <variable name> <- <initial value>" declares a variable that will be manually rebound, - "{section, section_counter + 1, lesson_counter}" - explicitly returns the usual value and the rebound values; fails if the tuple does not match the "let"s I'm not exactly happy about "{lessons, lesson_counter} =" - this can only work if we return the whole tuple from the for. But maybe that's better for consistency? Best, Stefan czw., 16 gru 2021 o 21:21 Amos King - Binary Noggin <a...@binarynoggin.com> napisał(a): > I think the `let` feature might have a big impact on the way that people > write code in Elixir. I don't know if that will be positive or negative. I > lean more toward expecting it to not be great. It feels more imperative. > Like setting a variable before a `for` loop in Java, C, ... > > That all said I have a small thought about the syntax if it is decided > that it moves forward. > Looking at the examples I think that defining something outside the loop > and mutating it in the loop feels awkward compared to how I have become > accustomed to Elixir and other functional languages. > > I actually think that Chris McCord's example where the `let` is defining > the initial value is much better. > > What about using a keyword syntax there, and not allowing the use of > variable declared above the `let`? > > for {k, v} <- Enum.reverse(list), reduce: acc: %{} do >> acc = Map.update(acc, k, [v], &[v | &1]) >> end >> >> Amos King, CEO > > <https://twitter.com/binarynoggin> > <https://www.facebook.com/BinaryNoggin/> > <https://www.instagram.com/binarynoggin/> > <https://www.linkedin.com/company/binary-noggin> > > 573-263-2278 a...@binarynoggin.com > > > > On Thu, Dec 16, 2021 at 1: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> >> . >> > -- > 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/CAJr6D4S3WfHgmryrz9o%3D1AFwebbLRfakUN6Vvi%3D5w8knYjVnPw%40mail.gmail.com > <https://groups.google.com/d/msgid/elixir-lang-core/CAJr6D4S3WfHgmryrz9o%3D1AFwebbLRfakUN6Vvi%3D5w8knYjVnPw%40mail.gmail.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/CACzMe7a%2B%3DyWRB_Tv8S3q48d1Tw9QrUUbxfD9-NWaAXkJ3SVm%2BQ%40mail.gmail.com.