> We could get cuter with the syntax by overloading guards: update map when condition do Map.put(map, :key, value) end
It was pointed out to me that this would either have to work only with guard-compatible conditions or be wildly inconsistent with the rest of the language, so I think this syntax is out On Friday, December 6, 2024 at 4:27:32 PM UTC-6 Christopher Keele wrote: > > One pattern I see repeated constantly in different apps developed by > myself or others is adding values to a map conditionally or returning the > map unchanged. > > I agree this is a wart common with maps in particular (as the > out-of-the-box update-often data structure), but the problem is not > specific to the Map API; rather, conditional expressions in Elixir. > > The intentional design decision for *if*-and-friends conditionals to > honor lexical scoping was not originally part of the language, but added > for consistency with other branching structures early on. So you in fact > used to be able to just do *map = %{}; if conditional, do: map = > Map.put(map, :foo, :bar)*. Changing this was controversial at the time > partially because of this knock-on effect of having to always exhaustively > handle all branches of a conditional if assigning results directly to a > variable (or otherwise only temporarily branching the control flow of the > current scope). > > TL;DR you have to do a lot more *foo = if ..., else: foo* to keep > conditional lexical scoping consistent, and I'm in agreement with José that > it's that slightly irritating *else: foo* that (if anything) should be > solved holistically at the core language level, rather than extending > individual data-structure's APIs. > ------------------------------ > > I don't think we can "solve" *else: foo* without discussing why it's a > problem. I can think of two rationales, but interested in other opinions: > 1. Accidentally omitting it can lead to unintentional nil assignments. > 2. It is syntactically noisy for what it accomplishes (from the > programmer's perspective, literally "nothing"—as in, leaving the assignment > in question the same). > > In my experience, 1. is not a huge issue, but others may have stronger > opinions. It's 2. that makes it a wart. The problem is that there is not > much more syntax to strip away from *if*: no else clause means *nil* and > that cannot reasonably change, and the rest of the macro does not > understand that there is a "subject" being assigned to for it to choose to > return unchanged. I would propose either *introducing a new conditional > assignment macro* (as discussed a little here already), or *consider > additional syntaxes for conditionals* that makes it a little easier > visually to ignore the fallback case. > > In either case, as José points out, we need to consider 3 components: a > *subject* to or to not update, a *condition*, and an *action*. > ------------------------------ > New Macro > > I agree with the criticisms of *then_if*. I would rather see something > explicitly about updating the subject. Say, a hygine-modifying > *update_when(subject, > condition, fn/block)* that required a variable reference subject. Ex: > > update_when(map, condition, &Map.put(&1, :key, value)) > > or > > update_when(map, condition) do > Map.put(map, :key, value) > end > > The pipe-ability of this is limited by design, but this could still work > with *then*: > changeset > |> do_some_checking() > |> then(fn changeset -> > update_when(changeset, changeset.valid) do > do_more(changeset) > end > end) > |> do_something_else() > > Honestly, not in love with this, but I'm slow to warm to these things. We > could get cuter with the syntax by overloading guards: > > update map when condition do > Map.put(map, :key, value) > end > > Reads better, technically parses, but kind of inconsistent with other > guard constructs conceptually. Also, how would piping work? Is there a way > for this to make sense in a larger pipeline: > > map > |> update when condition do > Map.put(map, :key, value) > end > ------------------------------ > Changing if > > Since *if* cannot be fundamentally aware of a subject, it would have to > have a place to specify the default fallback, which *else* already does > in this situation; it's as semantically dense as it can be. To alleviate > the noise the fallback block introduces, one option would be to have the > *if* macro accept optional keyword arguments before the block, merging > them together, allowing hoisting the trivial *else* case inline with the > condition, independent of the consequent, to create a denser syntax: > > map = if condition, else: map do > Map.put(map, :key, value) > end > > This also cannot really be piped through without *then*, but otherwise > reads (slightly) nicer than the base case: > > changeset > |> do_some_checking() > |> then(fn changeset -> > if changset.valid, else: changeset do > do_more(changeset) > end > end) > |> do_something_else() > > It's a really small change that I think pretty much fully addresses the > syntactic noise problem. It does lead to this rather odd formulation I'm > not sure about: > > condition > |> if(else: map) do > Map.put(map, :key, value) > end > ------------------------------ > Changing case/cond > > Of course, we do already have a conditional expression with a semantic > notion of a subject, *case*. However, there's no specific syntax for > referencing it, outside clause heads, so the programmer would have to > provide it again, similar to the fallback *_ -> subject* construct today: > > map = case map do > %{} -> Map.put(map, :key, value) > _ -> map > end > > I think this is orthogonal to the problem we are trying to solve, but if > we went the *if(conditional, else: fallback) do* route, we'd need to > consider if we should extend *case*/*cond* with similar semantics for > consistency's sake, so: > > case map, else: map do > %{key: old_value} -> Map.put(map, :key, old_value + 1) > %{} -> Map.put(map, :key, 0) > end > > Of course the problem here is that implies the existence of general *else* > clauses in those constructs: > > case map do > %{key: old_value} -> Map.put(map, :key, old_value + 1) > %{} -> Map.put(map, :key, 0) > else > map > end > > We could implement support this and have it compile down to the correct *_ > -> map* fallback case and warn/error if one was already provided > (similarly with *true -> map* for *cond*), but generally, not a fan of so > many ways to do the same thing. > ------------------------------ > This is less an argument for adding *else* to these constructs, and more > an argument for calling the keyword argument to *if* *something else* less > likely to be confused with block semantics. So I'd say that I personally am > warmest on the *if* proposal alone, and am open to calling the keyword > something different and merging it in with the block with the same *else* > duplication > warnings/errors we'd need regardless of name, like: > > map = if condition, fallback: map do > Map.put(map, :key, value) > end > > The *update subject when condition do* syntax sugar reads very nicely, > but feels like it would lead to confusion down the line. > On Friday, December 6, 2024 at 11:01:28 AM UTC-6 jimf...@gmail.com wrote: > >> then_if has no meaning to me and breaks my brain. >> >> Seems not to flow with other pipeline commands. >> >> Dr. Jim Freeze, Ph.D. >> ElixirConf® >> ElixirConf.com >> ElixirConf.eu >> (m) 512 949 9683 <(512)%20949-9683> >> >> >> On Fri, Dec 6, 2024 at 10:58 AM José Valim <jose....@gmail.com> wrote: >> >>> Thank you Zach. When I wrote the proposal I felt it was missing >>> something still and I think you nailed it. >>> >>> Passing two anonymous functions would help with the pipeline but it >>> feels it would be detrimental to other cases. >>> >>> >>> >>> *José Valimhttps://dashbit.co/ <https://dashbit.co/>* >>> >>> >>> On Fri, Dec 6, 2024 at 17:41 Zach Daniel <zachary....@gmail.com> wrote: >>> >>>> Despite typically being a "put it in the standard library" guy, I don't >>>> think that `then_if` actually composes as well as it looks like it does on >>>> the tin due to the fact that `then` is often used in pipelines, where some >>>> transformation has happened and you want to check a condition *on that >>>> result*. For example: >>>> >>>> ```elixir >>>> changeset >>>> |> do_some_checking() >>>> |> then_if(<is_valid>, &do_more/1) >>>> ``` >>>> >>>> I think that `then` is kind of "already" the composition tool that we >>>> need for expressive pipes. >>>> >>>> ```elixir >>>> changeset >>>> |> do_some_checking() >>>> |> then(fn changeset -> >>>> If changeset.valid do >>>> do_more(changeset) >>>> else >>>> changeset >>>> end >>>> end) >>>> ``` >>>> >>>> I can see an argument that it is very verbose, but its also about as >>>> flexible as it can get. My suggestion would be to, if added, have >>>> `then_if` >>>> take a function as its first argument. >>>> >>>> ```elixir >>>> changeset >>>> |> do_some_checking() >>>> |> then_if(&(&1.valid?), &do_more/1) >>>> ``` >>>> >>>> >>>> On Dec 6, 2024, at 10:30 AM, Ben Wilson <benwil...@gmail.com> wrote: >>>> >>>> Exploring what that looks concretely in this case: >>>> >>>> ``` >>>> map >>>> |> other_stuff >>>> |> then_if(opts[:foo], &Map.put(&1, :key, value)) >>>> ``` >>>> >>>> I like it! Conditional map insert helper functions are definitely >>>> something we've written over and over again in our code bases and while >>>> it's easy to do, I think in some cases this is cleaner looking than a >>>> proliferation of `maybe_put_foo` functions. >>>> >>>> - Ben >>>> >>>> On Friday, December 6, 2024 at 9:59:40 AM UTC-5 José Valim wrote: >>>> >>>>> Hi Juan! >>>>> >>>>> My initial gut feeling is that this approach does not scale. What if >>>>> you want to delete a key conditionally? Should we have delete_if? >>>>> >>>>> It feels a more general approach would be to introduce `then_if`: >>>>> >>>>> then_if(subject, condition?, function) >>>>> >>>>> Or similar. :) >>>>> >>>>> *José Valimhttps://dashbit.co/ <https://dashbit.co/>* >>>>> >>>>> >>>>> On Fri, Dec 6, 2024 at 3:27 PM Juan Manuel Azambuja < >>>>> ju...@mimiquate.com> wrote: >>>>> >>>>>> Hello, >>>>>> >>>>>> After working with Elixir for some time I have found myself repeating >>>>>> some patterns when dealing with maps. >>>>>> >>>>>> One pattern I see repeated constantly in different apps developed by >>>>>> myself or others is adding values to a map conditionally or returning >>>>>> the >>>>>> map unchanged. This comes in different flavors: >>>>>> >>>>>> [image: Screenshot 2024-12-06 at 11.13.23 AM.png] >>>>>> or >>>>>> [image: Screenshot 2024-12-06 at 11.14.32 AM.png] >>>>>> >>>>>> When this pattern gets used enough in an app, it's normal to see it >>>>>> abstracted in a MapUtils module that updates the map conditionally if a >>>>>> condition is met or returns the map unchanged otherwise. >>>>>> >>>>>> My proposal is to include Map.put_if/4 which would abstract the >>>>>> condition check and return the map unchanged if the condition is not met: >>>>>> >>>>>> [image: Screenshot 2024-12-06 at 11.17.21 AM.png] >>>>>> >>>>>> Enhancing the API by doing this will result in less code and more >>>>>> readable solutions. >>>>>> >>>>>> Thanks for reading! >>>>>> >>>>>> -- >>>>>> 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-co...@googlegroups.com. >>>>>> To view this discussion visit >>>>>> https://groups.google.com/d/msgid/elixir-lang-core/ed7da716-b9f5-4f64-a77d-d32696326b9en%40googlegroups.com >>>>>> >>>>>> <https://groups.google.com/d/msgid/elixir-lang-core/ed7da716-b9f5-4f64-a77d-d32696326b9en%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-co...@googlegroups.com. >>>> To view this discussion visit >>>> https://groups.google.com/d/msgid/elixir-lang-core/e9e799a2-ad69-4791-bd9a-22bca327652fn%40googlegroups.com >>>> >>>> <https://groups.google.com/d/msgid/elixir-lang-core/e9e799a2-ad69-4791-bd9a-22bca327652fn%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-co...@googlegroups.com. >>>> To view this discussion visit >>>> https://groups.google.com/d/msgid/elixir-lang-core/61753088-63E3-4DA0-8CEF-925149D789C6%40gmail.com >>>> >>>> <https://groups.google.com/d/msgid/elixir-lang-core/61753088-63E3-4DA0-8CEF-925149D789C6%40gmail.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-co...@googlegroups.com. >>> >> To view this discussion visit >>> https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JSE7vhfHukf2EZ6bmi4%3DNrfX28q3%2BKpQGZMgFoCM%3D%2BWg%40mail.gmail.com >>> >>> <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JSE7vhfHukf2EZ6bmi4%3DNrfX28q3%2BKpQGZMgFoCM%3D%2BWg%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 visit https://groups.google.com/d/msgid/elixir-lang-core/0d8078fc-64a7-45ab-9195-40416b70c600n%40googlegroups.com.