Keyword.validate! <https://hexdocs.pm/elixir/Keyword.html#validate/2> is meant to solve part of the option validation, except typing.
NimbleOptions can solve typing but ideally I would try to embed the basic type validation in the type system. So in the long term, I would say type system + Keyword.validate!. On Fri, Oct 28, 2022 at 7:05 PM Zach Daniel <zachary.s.dan...@gmail.com> wrote: > 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 > <https://groups.google.com/d/msgid/elixir-lang-core/l9squzpm.82ffe77f-1d5b-4c9f-9adf-83a8ed0cf0e8%40we.are.superhuman.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/CAGnRm4KA%2BzATOV%2BW7AQOXeKKkxxg%2BcCfOv-nztsPS5JRu6ezdg%40mail.gmail.com.