josevalim

josevalim

Creator of Elixir

Discussion: Incorporating Erlang/OTP 21 map guards in Elixir

Hi everyone,

Erlang/OTP 21 comes with two new guards contributed by @michalmuskala: map_get/2 and is_map_key/2. Now we need to discuss how we would integrate those in Elixir. This proposal will be broken in three parts, each with their own discussion. Feel free to comment on any conclusion individually or as a group, but please let us know why.

NOTE: this is a focused thread, so we appreciate if everybody stayed on topic. Feel free to comment anything in regards to maps and guards but avoid off-topic or loosely related topics. For example, if you would like to know when other OTP 21 features will be added to Elixir, please use a separate thread.

The map_get/2 and is_map_key/2 guards

The first topic to consider is if we want to add the map_get/2 and is_map_key/2 guards to Kernel. Here are some things to consider.

First of all, in Elixir we don’t duplicate Kernel functionality in other modules. For example, since we have Kernel.map_size/1, there is no such thing as Map.size/1. However, given that we have Map.fetch!/2 (equivalent to map_get/2) and Map.has_key?/2 (equivalent to is_map_key/2), adding those two new guards means we would break those rules, as we simply can’t remove the Map functions.

Second of all, I am not particularly sure that map_get/2 and is_map_key/2 are beneficial in actual code, since they don’t add anything that you can’t do with a pattern matching. Let’s see the website example, without guards:

def serve_drinks(%User{age: age} = user) when age >= 21 do
  ...
end

Now written with guards:

def serve_drinks(%User{} = user) when map_get(user, :age) >= 21 do
  ...
end

Or if we assume we don’t need to check for the struct name:

def serve_drinks(user) when map_get(user, :age) >= 21 do
  ...
end

In both cases, I personally prefer the original example. It is clearer and promote better and more performant practices (pattern matching instead of field access).

In my opinion, the real advantage of map_get/2 and is_map_key/2 is that we can use them to build composable guards. For example, now we can finally implement guards such as is_struct/1:

defguard is_struct(struct)
         when is_map(struct) and
                :erlang.is_map_key(:__struct__, struct) and
                is_atom(:erlang.map_get(:__struct__, struct))

Luckily, to implement those guards, we don’t need to add is_map_key/2 and map_get/2 to Elixir. We can just invoke them from the :erlang module.

Discussion #1: Should we add map_get/2 and is_map_key/2 to Kernel?

Expand defguard

If the main goal of map_get/2 and is_map_key/2 is to help in the construction of composable guards, then one possible route to take is to expand the defguard construct, introduced in Elixir v1.6, to also support patterns. Such that the following:

def serve_drinks(%User{age: age} = user) when age >= 21 do
  ...
end

Could be written as:

defguard is_drinking_age(%User{age: age}) when age >= 21

def serve_drinks(user) when is_drinking_age(user)

The guard above would internally be written as:

defguard is_drinking_age(user)
         when is_map(user) and
                :erlang.is_map_key(:__struct__, user) and
                :erlang.map_get(:__struct__, user) == User and
                :erlang.is_map_key(:age, user) and
                :erlang.map_get(:age, user) >= 21

We can translate almost any pattern to guards. The only exception are binary matches, which won’t be supported.

Discussion #2: Should we allow patterns in defguard/1?

Support map.field in guards

Yet another approach to take is to support only map.field in guards. Again, if we take the original example:

def serve_drinks(%User{age: age} = user) when age >= 21 do
  ...
end

We could write it as:

def serve_drinks(%User{} = user) when user.age >= 21 do
  ...
end

Or even as:

def serve_drinks(user) when user.age >= 21 do
  ...
end

Where the latest is, IMO, by far the most readable.

However, this also has its own issues. First of all, as we said in the first section, pattern matching is typically more performant and clearer. Second of all, the foo.bar syntax in Elixir can mean either map.field OR module.function. This may be the source of confusion since invoking remote functions is not allowed in guards and those will make the guard to fail. For example, if somebody passes serve_drinks(User) to def serve_drinks(user) when user.age >= 21 do, you will get a FunctionClauseError, as only passing maps would be supported. However, I personally don’t think this would be a large issue in practice, as guards have always been limitted, remote calls are never supported in guards and dynamic remote calls are rare anyway, especially zero-arity ones.

Discussion #3: Should we allow map.field in guards?

Most Liked

michalmuskala

michalmuskala

Discussion #1: Should we add map_get/2 and is_map_key/2 to Kernel?

Yes, we should. There are things that can’t be expressed with the other proposals (or any other way in a guard for that matter), most notably:

def check_key(map, key) when is_list(map_get(map, key)), do: ...

It’s effectively implementing the def foo(key, %{key => value}) ... that is such a frequent request - now we’d have a way to do this.

However, I’m not a huge fan of the map_get name (pretty ironic considering it’s me who contributed it to Erlang). It feels very “imperative” while I find pattern matching and guards to be mostly about declarative code.

For example, for tuples, we use elem, not get_elem and for lists hd not get_hd (though head would probably be better). Because of that, I think we should explore adding this function under some different name. Some proposals: field, map_field, map_value, key_value, …

Discussion #2: Should we allow patterns in defguard/1?

Yes, I think we should. I like this idea a lot.

The only problem is that we might hit some performance issues with complex patterns when the value is used multiple times. Since we can’t bind inside guards, we’d have to generate the “access” code at each place the value is used. It becomes especially problematic when coupled with the in operator. It might lead to gigantic code explosion. I don’t think compiler would be smart enough to eliminate all those common sub-expressions (a long-term solution to this would be compiling to core erlang that allows binding in guards, that’s another set of issues, though).

With all of that said, I don’t think this should prevent us from exploring the implementation of this feature.

Discussion #3: Should we allow map.field in guards?

No. This feature would break the consistency of the language and make some construct mean different things inside and outside guards.

It’s true, today there’s difference in how code fails inside and outside guards (e.g. hd([]) might fail with an exception or just fail the current guard clause). When the expression fails or succeeds is always the same, though - this would break it and break it completely silently.

As a counter example, I could easily imagine somebody trying to write code like that:

def update_or_rollback(repo, changeset) do
  case repo.update(changeset) do
    {:error, reason} when repo.in_transaction?() -> repo.rollback(reason)
    other -> other
  end
end

Which would just silently fail. And it’s not a completely artificial example - in one of the projects I have a very similar function:

def update_or_rollback(repo, changeset) do
  case repo.update(changeset) do
    {:error, reason} = error ->
      if repo.in_transaction?(), do: repo.rollback(reason), else: error
    other -> other
  end
end
13
Post #7
gregvaughn

gregvaughn

Discussion #1: Should we add map_get/2 and is_map_key/2 to Kernel?
no
Discussion #2: Should we allow patterns in defguard/1?
yes
Discussion #3: Should we allow map.field in guards?
no

The reason behind it is basically the same for each. We should encourage pattern matching. Having more ways of doing the same things will lead to more confusion.

12
Post #4
josevalim

josevalim

Creator of Elixir

Related to the guards discussion, Elixir master use the new documentation metadata to annotate when we have guards. We are leveraging this information to show Guards in their own section in the docs sidebar: https://hexdocs.pm/elixir/master/Kernel.html

josevalim

josevalim

Creator of Elixir

FWIW, I tend to use this rule: if the key is necessary when matching the pattern, keep it in the pattern, otherwise move it to the body. So I end-up with code like this:

def some_fun(%{field1: :value} = struct) do
  %{field2: value2, field3: value3} = struct
  ...
end

No. foo["bar"] also works for lists and it would be hard to argue why it works only for maps and not lists. It is probably another reason against map.field.

10
Post #6
michalmuskala

michalmuskala

One thing to consider is that even though Map.fetch!/2 and map_get/2 would have the same functionality, they are slightly different from the VM perspective. Most notably, Map.fetch!/2 is a regular function, so it can be traced, while map_get/2 is a guard BIF, so it cannot be traced. Additionally, since Map.fetch!/2 is a regular function, it requires setting up a stack in the caller and is potentially slightly more expensive.

Where Next?

Popular in Proposals Top

josevalim
NOTE: this is a focused thread, so we appreciate if everybody stayed on topic. Feel free to comment anything in regards to calendar forma...
New
josevalim
Hi everyone, This is a proposal for introducing local accumulators to Elixir. This is another attempt of solving the comprehension probl...
1043 9939 246
New
josevalim
Hi everyone, as you may be aware, we are researching a type system for Elixir. As preparation for my upcoming ElixirConf US talk, I woul...
New
josevalim
I am resubmitting the proposal from earlier today with more context and more focus on the important parts. Some concerns and praises stay...
452 17667 124
New
josevalim
UPDATE: This proposal has been retracted. Read the new proposal here: Local accumulators for cleaner comprehensions Hi everyone, This i...
New
josevalim
One of the major differences between running your application as a release and as a Mix project is the differences in configuration. Mix ...
382 17497 108
New
michalmuskala
TL;DR: The Elixir Core team is announcing a call for proposals to extend support for time zones in Elixir’s standard library. The reason...
New
josevalim
Hi everyone, Erlang/OTP 21 comes with two new guards contributed by @michalmuskala: map_get/2 and is_map_key/2. Now we need to discuss h...
New
josevalim
Hi everyone, We are considering deprecating 'charlists' in Elixir in favor of ~c"charlist". In many languages, 'foobar' is equivalent to...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New

Other popular topics Top

Tee
can someone please explain to me how Enum.reduce works with maps
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement