I’m struggling in elixir with the behavior of keys and nil values. But I see that the origin of the problem is partially in how Erlang defines maps:
1> Val = #{}.
#{}
2> Input@1 = case Val of
2> #{‘daps-PowerCoordinationInfo-r16’:=Input@1_0} → Input@1_0;
2> _ → asn1__MISSING_IN_MAP
2> end.
asn1__MISSING_IN_MAP
This code checks for the presence of a key in the map through matching. (Taken from generated asn1 codec.) You can see how it evaluates to key missing.
`3> Val2 = #{‘daps-PowerCoordinationInfo-r16’ => nil}.
#{‘daps-PowerCoordinationInfo-r16’ => nil}
4> Input@2 = case Val2 of
5> #{‘daps-PowerCoordinationInfo-r16’:=Input@1_0} → Input@1_0;
5> _ → asn1__MISSING_IN_MAP
5> end.
nil
If we provide a similar map with the key present but set to nil, the same code will evaluate to nil.
So “key presence” is different from “value of key is nil.” Which, if looked at by itself, is not a problem. It becomes however a problem in my view when looking at what distinctions you have to make when filling and evaluating maps.
Erlang is more consistent in this than elixir has built on it. If I look at the maps module, I see maps:get/2 will raise a badkey exception if the key is not present, maps:get/3 will respond with the default. For comparison, elixir’s Map.get/3 will be the same as maps:get/3, but Map.get/2 will return nil. This is more of an interop pitfall.
In elixir you have to be aware:
x = {} # empty map
x.key # throws if atom key is not a key in x
x[:key] # returns nil
Map.get(x, :key) # returns nil
Map.get(x, :key, “default!”) # returns “default!”
Map.has_key?(x, :key) # returns false
If you have a key present but it is nil:
x = { key: nil }
x.key # returns nil
x[:key] # returns nil
Map.get(x, :key) # returns nil
Map.get(x, :key, “default!”) # returns nil
Map.has_key?(x, :key) # returns true
This is still consistent, but becomes very unwieldy. I’m not trying to assert that elixir’s somewhat confusing semantics here are all rooted in Erlang, but I want to show, returning to the initial example, how they cause code to become hard to understand and convoluted:
We had code as follows:
{‘daps-PowerCoordinationInfo-r16’: is_nil(value) && :asn1__MISSING_IN_MAP || value }
Actually we were trying to update large maps (for ASN.1 encoding) in the declarative syntax. But there is no native way to say “This value is present only if.” So what was done here instead that we already put the value that the ASN.1 codec encode function would insert internally so that we could still could use the declarative syntax.
The ASN.1 codec does what I wrote above - it matches for the presence of the key. Both matching for the presence of a key in the map and calling elixir’s Map.has_key?/2 and Erlang’s maps:is_key/2 would all return true if a key was present but its value nil.
So, in most situations you have to make extremely fine distinctions whether the key is present or if it’s more important that its value is nil. elixir makes this a bit more confusing by some operator/function semantics that default to nil for the caller’s convenience. But the declarative syntax becomes impossible to use if there’s a scenario where the value might not be present, leading to a series of individual updates instead.
What we are currently contemplating in our codebase is either create a function that puts the value only if it is not nil and do the updates individually. Or a function that cleans up all keys from a map which have the value nil.
The question is: Is the general ability to have nil values in a map so important to actually convolute code built on it? Or would it also be possible to switch to a behavior where keys with value nil are never inserted in a map?
I’m sure scenarios exist where people will appreciate the presence of a key with value nil. But in case of the code base the inability to use declarative syntax to only insert an element if it was not nil has been a problem. (Same for using declarative syntax for lists.) We then write code for modifying maps or glueing together different lists and it never gets easy to understand.