Couldn’t anyone who wants to do something like this just use a tool like nimble_options (at least in the near to mid term)? That’s what I do. https://github.com/dashbitco/nimble_options
On Fri, Oct 28 2022 at 12:35 PM, Wiebe-Marten Wijnja < w...@resilia.nl > wrote: > > > > Thank you for starting this interesting discussion! > > > > While I don't think the suggested solution (introducing special pattern > matching syntax) is viable, for the reasons already mentioned by others, > I do think the problem itself warrants further consideration. > > > > Currently, handling keyword arguments is done in an ad-hoc fashion. > Approaches between different codebases and even between different parts of > the same codebase vary significantly. > Especially w.r.t. error handling. > Even in Elixir's own codebase this is apparent. Some (non-exhaustive) > examples: > - Passing wrong options to `if` raises an ArgumentError with the text > "invalid or duplicate keys for if, only "do" and an optional "else" are > permitted" > - Passing wrong options to `defimpl` raises an ArgumentError with the text > "unknown options given to defimpl, got: [foo: 10, bar: 20]" > - Passing wrong options to `for` raises a CompileError with the text > "unsupported option :foo given to for" > - Passing wrong options to `inspect` ignores the option(s) silently. > - Passing wrong options to `GenServer.start_link` ignores the option(s) > silently. > > > > Other differences are between whether only keyword lists are accepted, or > maps with atom keys also, or possibly anything implementing the `Access` > protocol. > And in some places the options are used to create a special struct > representing the parsed options, which is allowed to be passed as well > directly. > > > > > This makes me think that we might want to look into standardizing: > - How to indicate which options are mandatory and which options have > defaults. > - What kind of exception is raised when incorrect values are passed (and > with what message). > - By default raise whenever unrecognized options are passed; the > alternative of ignoring unrecognized options as an explicit opt-in choice. > > > > > > I think we could introduce a macro that embeds the code to do these things > and turn the result into a map inside the function where it is called. > For the reason mentioned by José before (supporting multiple function > clauses with different pattern matching and defaults) > it makes more sense to call this macro in the function body rather than > embellish the function head with some special form. > What I haven't been able to figure out yet is how to call this macro > (`parse_options`?), or in which module in Elixir core it should live. > (`Keyword`? Or in a new `Option` module?) > > I haven't written a proof-of-concept yet but I am pretty sure that it is > possible to write an implementation that needs to traverse the list --or > map-- > that is passed in to the function only once. (Stopping earlier when the > number of keys inside do not match.) > This should be performant enough for general usage. > If there is a problem, I think that raising an ArgumentError (but with a > different error message detailing what options are missing or > unrecognized) > might be the clearest way to indicate to the caller that they are using > the function incorrectly. > > The diligent reader might notice that there certainly is some overlap > between this new macro and initializing a struct with enforced keys. > > > > > > > > > ~Marten / Qqwy > > > > > > > > On 28-10-2022 16:20, Jake Wood wrote: > > >> So the original proposal here is for introducing a named parameter syntax. >> The reason I like named parameters is b/c the order of parameters doesn't >> matter – when they do matter it's easy for refactoring to introduce hard >> to catch bugs. Pattern matching has been proposed as the idiomatic way to >> achieve argument order not mattering. If I understand correctly, the >> recommendation is to stuff arguments into a map just before a function >> call that itself immediately destructures them. While this approach does >> address my primary concern (ie parameter order), it has to be slower, >> right? I can imagine this having a non-trivial effect in a pipeline on a >> hot-path. >> >> >> So the question for me, really, is how much quicker is passing ordered >> arguments vs creating then destructuring a map? If it's negligible then >> it's negligble, but if it's not then it would be nice to have an >> alternative. >> >> >> - Jake >> >> >> >> On Friday, October 28, 2022 at 9:47:31 AM UTC-4 José Valim wrote: >> >> >>> > Is this an expensive pattern because it generates a map only for the >>> next function to extract the keys and ignore the map? >>> >>> >>> It depends on what you are comparing it with. Compared to simply passing >>> arguments, it is likely slower. Compared to keyword lists, likely faster. >>> >>> >>> On Fri, Oct 28, 2022 at 3:41 PM Brandon Gillespie < bra...@cold.org > >>> wrote: >>> >>> >>>> >>>> >>>> Fair enough :) >>>> >>>> >>>> >>>> If I understand what you are saying: they are all maps because the source >>>> data comes from a map, and it's the method of extracting data from the map >>>> that differs (the algorithm), not the inherent nature of a map itself. >>>> >>>> >>>> >>>> I agree, and apologize for the mistaken assertion. >>>> >>>> >>>> >>>> However, what I didn't benchmark as i think about it, is what I often will >>>> see, which is the creation of a map simply to pass arguments — and this is >>>> more relevant to the request/need. The example was based on existing >>>> structs/maps and not creating them at each function call time. >>>> >>>> >>>> >>>> >>>> Instead, for example: >>>> >>>> >>>> >>>> def do_a_thing(%{key2: value2, key1: value1}) do ... >>>> >>>> >>>> >>>> >>>> I think it's becoming a common pattern to then construct the maps as part >>>> of the call, ala: >>>> >>>> >>>> >>>> do_a_thing(%{key1: 10, key2: 20}) >>>> >>>> >>>> >>>> >>>> Is this an expensive pattern because it generates a map only for the next >>>> function to extract the keys and ignore the map? >>>> >>>> >>>> >>>> >>>> -Brandon >>>> >>>> >>>> >>>> >>>> >>>> >>>> On 10/28/22 12:37 AM, José Valim wrote: >>>> >>>> >>>>> >>>>> >>>>>> >>>>>>> >>>>>>>> >>>>>>>> >>>>>>> >>>>>>> >>>>>>> >>>>>>> 1.79 times, as I read it, not 1.79us. And of course benchmarks being >>>>>>> highly subjective, now that I retooled it it's at 2.12x slower (see >>>>>>> notes >>>>>>> at the very bottom for likely reasons why). >>>>>>> >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>> >>>>> >>>>> Correct. What I did is to take a reference value of 1us and multiplied it >>>>> by 1.79, to say that at this scale those numbers likely won't matter. >>>>> >>>>> >>>>>> >>>>>>> >>>>>>> >>>>>>> The gist includes three scenarios: >>>>>>> >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>> >>>>> >>>>> Thanks for sharing. I won't go deep into this, as requested, but I want to >>>>> point out that the conclusion "maps are slower (significantly enough to >>>>> avoid)" is still incorrect for the benchmarks above. >>>>> >>>>> >>>>> >>>>> All of those benchmarks are using map patterns because both map.field and >>>>> Map.get are also pattern matching on maps. >>>>> >>>>> >>>>> map.field is equivalent to: >>>>> >>>>> >>>>> case map do >>>>> %{field: value} -> value >>>>> %{} -> :erlang.error(:badkey) >>>>> _ -> :erlang.error(:badmap) >>>>> end >>>>> >>>>> >>>>> >>>>> Map.get/2 is equivalent to: >>>>> >>>>> >>>>> case map do >>>>> %{field: value} -> value >>>>> %{} -> nil >>>>> >>>>> end >>>>> >>>>> To further drive this point home, you could rewrite the map_get one as: >>>>> >>>>> >>>>> def map_get(engine) do >>>>> map_get_take(engine.persist, engine, @take_keys, []) >>>>> end >>>>> >>>>> defp map_get_take(engine, persist, [a | rest], out) do >>>>> case {engine, persist} do >>>>> {%{^a => value}, %{^a => value}} -> >>>>> map_get_take(engine, persist, rest, [{a, value} | out]) >>>>> >>>>> _ -> >>>>> map_get_take(engine, persist, rest, out) >>>>> end >>>>> end >>>>> >>>>> defp map_get_take(_, _, [], out), do: out >>>>> >>>>> >>>>> And the numbers likely won't matter or be roughly the same. The point is: >>>>> you are effectively benchmarking different algorithms and not the >>>>> difference between map_get or map_pattern. >>>>> >>>>> >>>>> I am only calling this out because I want to be sure no one will have >>>>> "maps are slower (significantly enough to avoid)" as a take away from this >>>>> discussion. >>>>> >>>>> >>>>> > What if a syntax for matching on keyword lists that allowed for items in >>>>> any position was added to Elixir? Something like (just shooting from the >>>>> hip) `[…foo: bar]` ? Then you could have your cake and eat it too, right? >>>>> >>>>> >>>>> Valid patterns and guards are dictated by the VM. We can't compile keyword >>>>> lists lookups to any valid pattern matching and I would be skeptical about >>>>> proposing such because we should avoid adding linear lookups to patterns. >>>>> >>>>> >>>>> It is worth taking a step back. It is not only about asking "can we have >>>>> this feature?". But also asking (at least) if the feature plays well with >>>>> the other constructs in the language and if we can efficiently implement >>>>> it (and I believe the answer is no to both). >>>>> >>>>> -- >>>>> You received this message because you are subscribed to a topic in the >>>>> Google Groups "elixir-lang-core" group. >>>>> To unsubscribe from this topic, visit >>>>> https://groups.google.com/d/topic/elixir-lang-core/Dbl6CL5TU5A/unsubscribe >>>>> . >>>>> To unsubscribe from this group and all its topics, send an email to >>>>> elixir-lang-co...@googlegroups.com >>>>> . >>>>> To view this discussion on the web visit >>>>> https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4L37yu8KVbhuM0gNkVYOzCeoXaKzTBk4aY4OLLRdgRRLg%40mail.gmail.com >>>>> ( >>>>> https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4L37yu8KVbhuM0gNkVYOzCeoXaKzTBk4aY4OLLRdgRRLg%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-co...@googlegroups.com. >>>> To view this discussion on the web visit >>>> https://groups.google.com/d/msgid/elixir-lang-core/9f60ba0c-8403-e93f-d5fb-b3f55df88d14%40cold.org >>>> ( >>>> https://groups.google.com/d/msgid/elixir-lang-core/9f60ba0c-8403-e93f-d5fb-b3f55df88d14%40cold.org?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/01432858-e854-4747-921a-230e6bbd7489n%40googlegroups.com >> ( >> https://groups.google.com/d/msgid/elixir-lang-core/01432858-e854-4747-921a-230e6bbd7489n%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/50a4057e-1d53-77fe-6cf5-1d7804f32b8b%40resilia.nl > ( > https://groups.google.com/d/msgid/elixir-lang-core/50a4057e-1d53-77fe-6cf5-1d7804f32b8b%40resilia.nl?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/l9squzpm.82ffe77f-1d5b-4c9f-9adf-83a8ed0cf0e8%40we.are.superhuman.com.