Unexpected behavior of Map.merge(<map set>, ...)
Closed this issue · 2 comments
Elixir and Erlang/OTP versions
$ elixir --version
Erlang/OTP 28 [erts-16.0.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]
Elixir 1.18.4 (compiled with Erlang/OTP 28)
Operating system
Fedora Linux 42 (Workstation Edition) Linux lggram 6.15.8-200.fc42.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 24 13:26:52 UTC 2025 x86_64 GNU/Linux
Current behavior
Passing a mapset to Map.merge() results in a weird behaviour: no merging is happening and the mapset is returned either from the 1st or the 2nd function argument:
iex> Map.merge(MapSet.new(), %{1 => 1, 2 => 2})
MapSet.new([])
iex> Map.merge(MapSet.new(), MapSet.new([1,2,3]))
MapSet.new([1, 2, 3])
iex> Map.merge(MapSet.new([3,4,5]), MapSet.new([1,2,3]))
MapSet.new([1, 2, 3])Expected behavior
The reason this is surprising is because incorrect results are returned and no type error is raised. For example, trying to pass a value of a completely different type to Map.merge() does result in an error, as it should:
iex> Map.merge([], %{1 => 1, 2 => 2})
** (BadMapError) expected a map, got: []
(stdlib 7.0.1) :maps.put(:k, :v, [])
(stdlib 7.0.1) erl_eval.erl:466: :erl_eval.expr/6
(elixir 1.18.4) src/elixir.erl:364: :elixir.eval_forms/4
(elixir 1.18.4) lib/module/parallel_checker.ex:120: Module.ParallelChecker.verify/1
(iex 1.18.4) lib/iex/evaluator.ex:336: IEx.Evaluator.eval_and_inspect/3
(iex 1.18.4) lib/iex/evaluator.ex:310: IEx.Evaluator.eval_and_inspect_parsed/3
(iex 1.18.4) lib/iex/evaluator.ex:299: IEx.Evaluator.parse_eval_inspect/4
(iex 1.18.4) lib/iex/evaluator.ex:189: IEx.Evaluator.loop/1
iex> Map.merge(MapSet.new(), 123)
** (BadMapError) expected a map, got: 123
(stdlib 7.0.1) :maps.merge(MapSet.new([]), 123)
iex:1: (file)
iex> Map.merge(MapSet.new(), [])
** (BadMapError) expected a map, got: []
(stdlib 7.0.1) :maps.merge(MapSet.new([]), [])
iex:1: (file)
iex> Map.merge(MapSet.new(), [1, 2])
** (BadMapError) expected a map, got: [1, 2]
(stdlib 7.0.1) :maps.merge(MapSet.new([]), [1, 2])
iex:1: (file)I would expect Map.merge() to raise when a mapset is passed to it.
Given a MapSet is a struct, and that structs are maps, this is an expected behavior (unfortunately, since it is clearly a footgun).
Map.merge(MapSet.new(), %{1 => 1, 2 => 2}) == %{:__struct__ => MapSet, :map => %{}, 1 => 1, 2 => 2}The type system should be able warn in such cases at compile time once we add support for typed structs, but at runtime since it's just calling erlang's :maps.merge, I think this is bound to happen.
We could perhaps also improve the inspect protocol to show ill-formed structs (with some wrong fields, e.g. 1 in the example above), at least for built-ins like mapsets/regexes etc?
Yes, let's improve inspect to be more assertive here.