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 atopic 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.
OpenPGP_signature
Description: OpenPGP digital signature