Thank you; I had missed the introduction of `Keyword.validate!` 😊

What is your stance on enhancing it to turn the result into a map or struct? Both for performance (performance characteristics of small maps are similar to those of tuples). And to make more illegal states unrepresentable: `Keyword.validate!` does not allow duplicate keys, but the output is another keyword list, which does not enforce this invariant. (i.e. "parse, don't validate <https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/>")


~Qqwy / Marten

On 28-10-2022 19:31, José Valim wrote:
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 <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4KA%2BzATOV%2BW7AQOXeKKkxxg%2BcCfOv-nztsPS5JRu6ezdg%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.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/elixir-lang-core/43090be2-323f-eb92-44a1-0d1adb417c0b%40resilia.nl.

Attachment: OpenPGP_signature
Description: OpenPGP digital signature

Reply via email to