Sometimes I remember the Access module and how woefully underutilized it is.
What if we added `Access.skip_if` and `Access.skip_where`? Usage would look like this: ```elixir map |> put_in([Access.skip_if(false), :key], value) |> put_in([Access.skip_where(fn map -> map.valid? end), :key], value) ``` > On Dec 6, 2024, at 5:27 PM, Christopher Keele <christheke...@gmail.com> 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 <https://elixirconf.com/> >> ElixirConf.eu <http://elixirconf.eu/> >> (m) 512 949 9683 <tel:(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é Valim >>> 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é Valim >>>>>> 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: >>>>>>> >>>>>>> >>>>>>> or >>>>>>> >>>>>>> >>>>>>> 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: >>>>>>> >>>>>>> >>>>>>> >>>>>>> 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 > <mailto: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 > > <https://groups.google.com/d/msgid/elixir-lang-core/a2b730c9-67e8-44d0-be8f-22ba4761fea7n%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 visit https://groups.google.com/d/msgid/elixir-lang-core/7E1B3911-34FA-4BA6-A0EF-452223B2546A%40gmail.com.