Discussion: Incorporating Erlang/OTP 21 map guards in Elixir

Kernel module is currently mentioned in the documentation. This is taken from the documentation of the Map module:

This module aims to provide functions that perform operations specific to maps (like accessing keys, updating values, and so on). For traversing maps as collections, developers should use the Enum module that works across a variety of data types.

The Kernel module also provides a few functions to work with maps: for example, Kernel.map_size/1 to know the number of key-value pairs in a map or Kernel.is_map/1 to know if a term is a map.

1 Like

Correct. Kernel was used to be bigger but we shrunk it before 1.0. The consensus was to keep those in Kernel precisely because they are the only ones available in guards.

2 Likes

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.

9 Likes

Completely uninformed vote, just expressing my personal style which is very much PM oriented, thusly, w/o surprise:

  1. no
  2. yes
  3. no
1 Like
  1. No
  2. Yes
  3. Abstain

Thanks everyone for the feedback.

For now, we will go with extending defguard/1 (discussion #2) and adding the is_struct and is_exception guards. For the remaining ones, we will wait until more use cases arrive, if ever.

Note though those enhancements will take a while to arrive, since they require Erlang/OTP 21, and the first Elixir release that will support exclusively Erlang/OTP 21+ is likely 1.10, which is about 1.5 years from now.

You can track Erlang/OTP 21 support in the issues tracker: https://github.com/elixir-lang/elixir/issues/6611

6 Likes

You probably mean 2.1.0 release, right?

No, the dots in a release are not on base 10. :slight_smile: After 1.9 you have 1.10.

2 Likes

@josevalim ah, my bad then
On github I can see only 1.7.0, 1.8.0 and 2.0.0 milestones, so I assumed you mean 2.1.0 as 1.8.0 therefore would be last 1.x.y release.

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

No. But for a different reason than what other people have been saying:

I think it is important to note here that the opposite is already happening: There exist some non-Kernel functions that are allowed in guards (Integer.is_odd and Integer.is_even come to mind; maybe there are others I cannot think of right now).

So I think that the most consistent way ( Where consistent is meant as: ā€˜Keeping in line with current decisions made in the standard libraryā€™) forward would be to alter the implementation of Map.fetch! and Map.has_key? as delegating to the :erlang.map_get and :erlang.is_map_key guards, meaning that Map.fetch! and Map.has_key? can be used inside guards.

They live outside of the Kernel for the same reason as Integer.is_odd does: People will not use them that often (we want them to usually use pattern matches instead).

I donā€™t think @michalmuskala 's concern of them not being traceable would be a problem, because these functions are never used in isolation.

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

Yes, Definitely. This would make writing guards a dream! :smiley:

About the performance problems: It seems to me that people might be complecting multiple things:

  • Speed of compilation. Which, unless extremely unreasonably slow, should not be that big of an issue.
  • Speed of runtime. My question to you: Would this be faster or slower than the current state of affairs? (Where non-guard constructs like ā€˜condā€™ are used to fake struct lookup)

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

No. Just like some other people have already said, the ā€˜freedom of syntaxā€™ that this gives people can be considered harmful; there are other, more explicit ways to do these tests.

1 Like

This is different because those are implemented on top of guards. They are literally macros. It can be implemented by anyone, anywhere, anytime. It is different from adding map_get and has_key which would be new primitives.

To be more precise, you need to require Integer before using Integer.is_odd/2. Imagine now if we had to require Map before using Map.fetch! or Map.has_key?? For starters, it would be backwards incompatible.

4 Likes

My opinion:

#1 - After reading Michalā€™s argument Iā€™m inclined to say yes too, but at the same time the thing Iā€™m still not sure, although I know Iā€™ve tried to write variants of def func(key, %{key => value}), is if thereā€™s any situation where you wouldnā€™t instead be explicit about the keys - i.e. youā€™ll end up having to differentiate/pattern match on the possible values that key will assume either way?

Perhaps it would allow more succinct/explicit code in certain situations, but without seeing a ā€œfullā€ program tree where it really made sense and you wouldnā€™t be able to write it otherwise itā€™s difficult. On the other hand, Iā€™m not sure thereā€™s anything to loose (mostly didactically) in introducing them and perhaps allowing as well, as JosĆ© says, to write def func(key, %{key => value}) and have it compiled to map_get under the cover (explicitly stating it in the docs).

#2 - I donā€™t particularly use defguards so I donā€™t really have much of an opinion here, I see many people do, so I would assume itā€™s something wanted and useful (and perhaps I will use them in the future too) and it also makes it more consistent to be able to use pattern matching everywhere.

#3 - Not really sure - It looks clean in isolation but it would be a replacement for explicit pattern matching, and I think that pattern matching is the idiom that is more consistent and clean overall, and because it only works with atom keys I donā€™t think it makes that much sense and can add to confusion when using it. In JS land thereā€™s no atoms, so you can switch the access syntax at will when writing, and there it makes a lot of sense. Also, if thereā€™s the opportunity to write it in guard clauses you can possible end up with moving all of those checks to the guard part of the function making the code less readable - at the same time, if you just want to discern between a key value being different (!=) from a certain value you would always write a pattern match on the key, and then a guard for that assertion either way, so it would be cleaner to just write a guard that handled both in one expression. I donā€™t see a need for it behind ā€œhypotheticalā€ situations so Iā€™m saying noā€¦

Either way, I would be totally fine with all of those changes if they were introduced, I also donā€™t think thereā€™s much too loose by introducing flexibility to the language (other than didactically), these donā€™t seem to be dramatic changes but instead improvements on the current syntax that go hand in hand with the latest introductions to OTP21, but thereā€™s nothing wrong with having artificial constraints that lead to better written code either.

You are absolutely right; I stand corrected :smiley:.

I am still thinking No for the first question (adding the guards to the Kernel), because removing something is a lot harder than adding something later if it turns out suitable use-cases exist. And until that time, people that really need it can use the versions exposed by :erlang instead.

Agree with everything said in the first message.

Discussion #1: no.

Pattern matching looks better and itā€™s generally better for a language to donā€™t have multiple ways to do the same simple thing in a multiple ways.

However, leveraging Erlang guards to support ${key => value} would be nice. We can explore that separately.

Discussion #2: yes

It would be better if defguard would allow using similar syntax to the one in function head.

Discussion #3: no

Same as 1. We already have a good way to handle those cases, donā€™t see a reason to add another one. The language should stay consistent.

I created a discussion about this.

https://elixirforum.com/t/proposal-introduce-a-way-to-use-new-beam-features-sooner/14849

5 Likes

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

This is unnecessary since there is already a generic way to obtain the same result using defguard/1.

Discussion #2: Should we allow patterns in defguard/1?
yes, for the reason above

Discussion #3: Should we allow map.field in guards?
no, if we go for defguard/1

Generally it seems like a better idea to have one way to have custom guards using defguard/1. Another argument would be: if another guard gets added to erlang, they will have to be added to Elixir as well, and maybe this will not be necessary but still will break consistency.

A post was split to a new topic: Why are there different naming styles?

Thanks for filling me in.

I have a few more thoughts for making the documentation more intuitive for those new to Elixir, which Iā€™ll link to as a separate thread when I have the spare time.

@josevalim https://groups.google.com/forum/?utm_source=digest&utm_medium=email#!topic/elixir-lang-core/SAejGg2e-io

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

11 Likes