Trouble understanding a warning: incompatible types

The following code results in a warning that is difficult for me to understand:

defmodule Mod do
  def hello(name) do
    map = Map.new(words: ["world"])

    IO.inspect(length(map.words), label: :before)
    map = %{map | words: [name | map.words]}
    IO.inspect(length(map.words), label: :after)
  end
end

This yields the following warning:

warning: incompatible types:

    %{words: [var1 | var2]} !~ %{words: var2}

in expression:

    # lib/mod.ex:7
    map.words

where "map" was given the type %{words: [var1 | var2], words: var2, optional(dynamic()) => dynamic()} in:

    # lib/mod.ex:6
    map = %{map | words: [name | map.words]}

where "map" was given the type %{words: [var1 | var2], optional(dynamic()) => dynamic()} (due to calling var.field) in:

    # lib/mod.ex:7
    map.words

HINT: "var.field" (without parentheses) implies "var" is a map() while "var.fun()" (with parentheses) implies "var" is an atom()

Conflict found at
  lib/mod.ex:7

The warning goes away if:

  1. The first IO.inspect with label: :before is commented out. Then the following two
    lines (6 and 7) are not longer at odds with each other type-wise.

  2. OR: Leave the IO.inspect as-is, but change the line following it (the map update) to
    one of the following:

map = %{map | words: [name] ++ map.words}
# or
map = %{map | words: [name | map[:words]]}
  1. OR: Use a struct instead of a dynamic map.

  2. OR: change the first IO.inspect to inspect map[:words] instead of map.words

So the warning is about conflicting types for %{words: [var1 | var2]} !~ %{words: var2}

  • line 6 → %{words: [var1 | var2], words: var2, optional(dynamic()) => dynamic()}
  • line 7 → %{words: [var1 | var2], optional(dynamic()) => dynamic()}

both mention optional(dynamic()) => dynamic(), the first seemingly only for var2 in the prepending of the list, the latter for the list itself?

In both cases, map is a (dynamic) map, the atom key :words’s value is a list so I expected
both to be of the same type, since [var1 | var2] is prepending var1 to the list var2,
much like the [var1] ++ var2 variant. This is what confuses me.

It all seems related to using the dot-notation to access an atom key in a dynamic map.

The docs say:

To access atom keys, one may also use the map.key notation. Note that map.key will raise a KeyError if the map doesn’t contain the key :key , compared to map[:key] , that would return nil .

as well as:

The two syntaxes for accessing keys reveal the dual nature of maps. The map[key] syntax is used for dynamically created maps that may have any key, of any type. map.key is used with maps that hold a predetermined set of atoms keys, which are expected to always be present. Structs, defined via defstruct/1, are one example of such “static maps”, where the keys can also be checked during compile time.

So should I read the above to always use map[key] for dynamic maps even if the key is an atom, and map.key only for static maps like a struct?

The code seems fine, I think this one is a bug in the compiler.

Opened an issue: Incorrect incompatible type warning on map dot access · Issue #13335 · elixir-lang/elixir · GitHub

3 Likes

Ok, thanks.

For future readers: this was already fixed in 1.16 (I was on 1.15.7).