Skip to content

Keyword.keys/1 accepts non-keyword lists and returns their keys #10010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sashaafm opened this issue May 1, 2020 · 1 comment · Fixed by #10214
Closed

Keyword.keys/1 accepts non-keyword lists and returns their keys #10010

sashaafm opened this issue May 1, 2020 · 1 comment · Fixed by #10214

Comments

@sashaafm
Copy link
Contributor

sashaafm commented May 1, 2020

Environment

Current behavior

iex(7)> Keyword.has_key?([{"my_key", 42}], "my_key")
** (FunctionClauseError) no function clause matching in Keyword.has_key?/2

    The following arguments were given to Keyword.has_key?/2:

        # 1
        [{"my_key", 42}]

        # 2
        "my_key"

    Attempted function clauses (showing 1 out of 1):

        def has_key?(keywords, key) when is_list(keywords) and is_atom(key)

    (elixir) lib/keyword.ex:810: Keyword.has_key?/2
iex(7)> Keyword.keys([{"my_key", 42}])
["my_key"]

as you can see in the above snippet when using Keyword.has_key?/2 it properly results in an error saying it failed to match any clause because the key must be an atom (as shown in is_atom(key)). However, in the following function Keyword.keys/1 it returns the "my_key" key which is not an atom.

This behaviour feels inconsistent to me. Because, AFAIK, Keyword.t represents a list of pairs {atom(), term()}, as seen here:

iex(11)> t Keyword
@type key() :: atom()

@type t() :: [{key(), value()}]

Going further down this path I've noticed that the lists.keymember/3 function from Erlang's core library also provides inconsistent behaviour:

iex(8)> :lists.keymember("my_key", 1, [{"my_key", 42}])
true

EDIT: this snippet above is actually correct because in Erlang's lists the key can be anything as per the docs.


Finally, looking at Elixir's implementation of Keyword.keys/2 here this seems to result due to using Erlang's lists module which probably is the root cause of these behaviours.

Expected behavior

The Keyword module only accepts actual Keyword.t().

Solution

I'm not sure what the solution here would be since Elixir is a dynamic language and we don't know if a given list respects the Keyword.t spec before actually evaluating it. Maybe, check if each key is an atom and raise an error otherwise? Or just return the keys which are atom?

@michalmuskala
Copy link
Member

michalmuskala commented May 1, 2020

In general the typespecs and documentation represent the indented use. The function can happen to work on other things as well due to implementation details, but this is not guaranteed and shouldn't be relied upon.

Specifically some functions in Keyword do enforce keys to be atoms where performance impact of doing this is negligible, but where it isn't they don't.

eksperimental added a commit to eksperimental-forks/elixir that referenced this issue Jul 20, 2020
Previously it would return something like this:

** (FunctionClauseError) no function clause matching in anonymous fn/1 in Keyword.keys/1

    The following arguments were given to anonymous fn/1 in Keyword.keys/1:

        # 1
        {"fetch/2", {:fetch, 2}}

Closes elixir-lang#10010, and it improves 3fd68ef
josevalim pushed a commit that referenced this issue Jul 21, 2020
…10214)

Previously it would return something like this:

** (FunctionClauseError) no function clause matching in anonymous fn/1 in Keyword.keys/1

    The following arguments were given to anonymous fn/1 in Keyword.keys/1:

        # 1
        {"fetch/2", {:fetch, 2}}

Closes #10010, and it improves 3fd68ef
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

2 participants