Kernel.struct() doesn't work with a map

Maps also implement the Enumerable protocol

https://hexdocs.pm/elixir/1.16.0/Map.html

And, Kernel.struct is defined like this:

Kernel
struct(struct, fields \ [ ])
@spec struct(module() | struct(), Enum.t()) :: struct()

https://hexdocs.pm/elixir/Kernel.html#struct/2

The second argument of struct/2 must be of type Enum.t(). Let’s give it a whirl:

defmodule MyStruct do
  defstruct [:a, :b]
end

defmodule B do
  def go do
    struct_struct = %MyStruct{a: 3, b: 4}
    IO.inspect(struct_struct, label: "[struct_struct]")

    map = %{"a" => 1, "b" => 2}
    map_struct = Kernel.struct(MyStruct, map)
    IO.inspect(map_struct, label: "[map_struct]")
  end
end

In iex:

iex(1)> B.go
[struct_struct]: %MyStruct{a: 3, b: 4}
[map_struct]: %MyStruct{a: nil, b: nil}
%MyStruct{a: nil, b: nil}

Why didn’t the map get converted to a struct?

map keys need to be atom key… as mentionned in the help

iex> h Kernel.struct
...
Keys in the Enumerable that don't exist in the struct are automatically
discarded. Note that keys must be atoms, as only atoms are allowed when
defining a struct.

You could do something like this…

    fields = MyStruct.__schema__(:fields) |> Enum.map(& to_string(&1))
    Enum.reduce(map, %MyStruct{}, fn {key, value}, acc ->
      if to_string(key) in fields do
        key = if is_binary(key), do: String.to_atom(key), else: key
        %{acc | key => value}
      else
        acc
      end
    end)
4 Likes

Ha. The answer was right in front of my face, and I didn’t see it. I noticed that Ecto.Changeset.cast has this type spec:

@spec cast(
Ecto.Schema.t() | t() | {data(), types()},
%{required(binary()) => term()} | %{required(atom()) => term()} | :invalid,
[atom()],
Keyword.t()
) :: t()

The type:

%{ required(binary()) => term() } | %{ required(atom()) => term() }

seems to specify a map with either double quoted string keys or atom keys. I’ve never seen required() before in a type, and I’m not sure how the following would be different:

%{ binary() => term() } | %{ atom() => term() }

They’re the same, but one implicitly requires key/value pairs of those types and the other notation is explicit about the requiredness. There’s also optional(…) as the counterpart to required(…).

https://hexdocs.pm/elixir/typespecs.html#literals

1 Like

Isn’t that the same as saying this spec:

@spec go(string())

implicitly requires a string type for the argument, but this spec

@spec go(required(string()))

explicity requires a string type? Isn’t the whole point of providing a type spec to list the required types?

It’s not the same. If you look at the documentation linked you’ll see that those calls are only used in combination with typespecs for maps.