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.

Attachment: OpenPGP_signature
Description: OpenPGP digital signature

Reply via email to