brettp
Elixir allows Map functions to manipulate structs
We ran into an issue today with structs that I found a bit surprising.
The essence of the issue is:
- A
structis aMapunder the hood - you can manipulate a
structwithMapfunctions, deleting keys that should be there and adding keys that shouldn’t - as long as the
__struct__key is still there, theMapis treated as astructincluding in Elixir pattern matching
Here’s some code which demonstrates this:
defmodule Sandbox do
@enforce_keys [:mandatory]
defstruct [:mandatory, :optional]
def struct_foo do
s1 = %Sandbox{mandatory: "must have this"}
do_something("s1", s1)
s2 = Map.delete(s1, :mandatory)
do_something("s2", s2)
s3 = Map.put(s1, :bogus, "blah")
do_something("s3", s3)
end
def do_something(s, %Sandbox{} = thing) do
IO.puts "#{s} #{inspect thing} is a %Sandbox"
end
def do_something(s, thing) do
IO.puts "#{s} #{inspect thing} is just a thing"
end
end
iex(24)> Sandbox.struct_foo()
s1 %Sandbox{mandatory: "must have this", optional: nil} is a %Sandbox
s2 %{__struct__: Sandbox, optional: nil} is a %Sandbox
s3 %{__struct__: Sandbox, bogus: "blah", mandatory: "must have this", optional: nil} is a %Sandbox
One interesting thing is that IO.inspect seems to know the difference between the original struct and one that’s been hacked into a Map, but Elixir pattern matching doesn’t.
So you almost certainly should not be manipulating a struct in this way, and you certainly shouldn’t be deleting keys from structs. The question is: should (could) Elixir do anything to stop you doing this? Should Map functions check for __struct__ and behave differently if they are asked to do something nefarious to a struct? Should a Map function which manipulates a struct at the very least also remove the __struct__ key so it returns a Map that is not treated as a struct any more?
Most Liked
josevalim
Correct.
It could but it would make all maps operations more expensive, which is mostly why we don’t. If we had a static type system, doing Map.put/3 on a struct would certainly fail.
rodrigues
A follow up on the issue described above: turns out it was really a bug in erlang, and Hans Bolinder from Ericsson fixed it today
dialyzer: Handle maps:remove/2 better by uabboli · Pull Request #2392 · erlang/otp · GitHub
tme_317
Adding to this point wanted to say I found this library to be awesome for succinctly defining typespecs, enforced keys, and default values on structs so I now use it for every struct… GitHub - ejpcmac/typed_struct: An Elixir library for defining structs with a type without writing boilerplate code. · GitHub
It doesn’t solve all the runtime concerns of the OP but it helps dialyzer help you without too much ceremony!








