> 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/a2b730c9-67e8-44d0-be8f-22ba4761fea7n%40googlegroups.com.