Why do Elixir maps have an update syntax but no insert syntax, meanwhile Erlang has both?

We all know that we’re able to build a map using the literal syntax using %{key1 => value1, key2 => value2}, as well as update existing keys of a map with %{map | key2 => new_value2}. But what reason could there be for not allowing us to insert new key-value pairs into a map using the literal syntax?

Inserting and updating are both supported (simultaneous) operations in Erlang using the literal syntax where the update is denoted by the := operator and insertion by the => operator.

So what is stopping Elixir from having something analogous?

2 Likes

I don’t know, but how do you think this should look in Elixir?

Map#{a := update, b => new}

I frequently struggle to remember when to use which operator in the Erlang syntax. So my goal was to introduce syntax for the struct-like API (i.e. you expect the keys exist and you can only update them) and leave the dictionary-like API (adding and removing keys) in the Map module. So you can do Map.put/3 to add, Map.delete/2 and so on.

9 Likes

If we are in topic was there a discussion to allow insert of map keys within update struct syntax? That may be a good resource to read …

If there was, it was a long time ago, before 1.0, on the elixir-lang-core mailing list.

1 Like

I honestly can’t tell you, although I also don’t think it’s relevant here. Half of that is already implemented in Elixir today, namely Map#{a := update} as %{map | a => :update}, so I don’t see that as being the blocker. I’m just trying to understand why something analogous to Map#{ b => new} doesn’t exist in Elixir.

But there isn’t anything specific about the update syntax which makes it exclusively for structs since you’re able to update maps whose keys are of any type.

Yes, exactly. Which is why I said “struct-like” but indeed, it works for regular maps too.

So I guess I still don’t understand why that syntax isn’t included. It sounds like you’re saying the reason for Elixir not having the map literal insert syntax is because your thought was that insertions are more aligned with dictionary operations and thus should only be done through the Map module. Yet the update syntax is allowed, which is counterintuitive to me because I would think if one is supported then the other should be as well due to the close relationship between the two operations, no? Otherwise, why not restrict its use to only structs where insertions are invalid?

An Elixir struct is a map with a known set of atom keys. For a struct, an insert would not make much sense, but updates are very common. I don’t know about others, but I only use the shorthand update syntax for structs, ie: %{struct | field: value}, For plain maps, I always use the Map.put/3,

Elixir uses : for atom keys for developer ergonomics.If Elixir need to support both insert and update in shorthand syntax, there will be 2*2=4 different notations, which would be hard to remember.

If you attempt to update a field which doesn’t exist in a struct using the update syntax, an exception will be thrown. I don’t see why that logic couldn’t be extended when attempting to insert a key into a struct if the syntax were supported.
Whether it is a struct or map, I always prefer the update syntax if I know the key must exist. I want to know if the state is contrary to what I expect and Map.put doesn’t allow for that. I believe that put should strictly be used for inserting.

Why would a second operator automatically double the number of notations? Why couldn’t it just add one more notation?
Also, for the purposes of this discussion, I’m not concerned with the implementation details. I’m only interested in understanding how Elixir came to the conclusion to omit the insert syntax.

Structs always have all defined keys present, so inserting a new key into a struct doesn’t make sense and shouldn’t be a supported use case. This would mean the syntax is only for maps — for structs it would be (1) equivalent to the update syntax when using the form %Struct{struct | new_key => new_val}, and (2) would be able to break structs when using the form %{struct | new_key => new_val} (i.e. without compile time validation of keys).

Map.put can be used to break structs already, but at least it’s in the Map module which hints that it’s not the first tool to use for structs.

1 Like

Which is precisely why I said an exception should be thrown in the case of structs, exactly as it would when attempting to update an invalid key in a struct. I’m not advocating for its use with structs, only in simple maps which is this discussion’s intention.

I’m also operating under the assumption that the update and insert operators would be different, as they are in Erlang, so I don’t believe the rest of your response applies.

I believe you got your answer earlier:

To me that’s the why: to make things more intuitive/simpler. And, the following is the how:

1 Like

The insert syntax in Erlang is equivalent to :maps.put, right? You could technically only use the insert syntax without the update syntax, and it would always work. I think a more useful syntax would be equivalent to Map.put_new! (which doesn’t exist), that way you would be explicitly saying you want to put a new value that doesn’t exist, or update one that does. That would be inconsistent with the Erlang behaviour, though.

Erlang: You can create new values and update existing values at once, but if you “create” an existing value by mistake, it’ll overwrite it.
Elixir: You can only update existing values, you can’t create new ones.

Because there are still many situations where you have a map that behaves like a struct (i.e. it only has a pre-defined set of keys) but you don’t need to define a struct for it. For example, the state of a GenServer.

Right. And honestly, I be alright with the Erlang behaviour since it would then be consistent with Map.put/:maps.put and thus with Erlang in general. For me it’s about using the appropriate operator based on what you expect the data to look like at that moment.

This is exactly one of the use cases that I encounter which lead me to this question. Usually I’ll have a pre-defined set of keys with one or two optional keys that do not have suitable default values to be included as part of the pre-defined set. In these situations, I’ve felt it would be beneficial to have the insert syntax along with update syntax.

Since you are the one that challenged the status quo, the onus is on you to give a detailed proposal. Mind you, it needs to be:

  • backward compatible
  • consistent with the existing syntax

In my point of view, the Erlang way makes sense because Erlang has a focus on conceptual clarity. The Elixir way also makes sense because Elixir has a focus on developer ergonomics. Sometimes you cannot achieve both.

If I was a language maintainer, my problem with such proposals would be that introducing syntax is a slippery slope that might never end.

So you would like to have a map inserting syntax. Cool. That’s reasonable, and I found myself wanting that in the past as well. But then somebody else comes along and says “Hey, what about a new syntax as a shortcut for Map.put_new and Map.put_new_lazy as well? I use these every day in my work!”. And you will find yourself in the very uncomfortable situation of having to explain why introducing new syntax X is reasonable but syntax Y is not.

(And let’s not even mention the maintenance burden of every new syntax, which is a very long topic by itself, but suffice to say they carry non-trivial opportunity costs.)

I agree with you that Elixir’s current choices and their tradeoffs might seem inconsistent for a newcomer or an outsider but I believe I speak for many people when I say that I’d still prefer knowing those choices and tradeoffs – and them being a reasonably low number – than to have more and more syntax that absolutely will confuse everyone but the biggest syntax advocates (and I’d argue they are likely below 1% of the language’s user base).

Elixir has quirks. All languages do. ¯_(ツ)_/¯ Elixir’s are not many though and are thus graspable and can easily be worked around.

2 Likes