2
votes

I have some struct definition

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

And I have some struct

a = %Foo{a: 1, b: 2}

I'm able to get new struct using pipe (like map)

%Foo{a | b: 3}
%Foo{a: 1, b: 3}

But unlike map, I can't get new struct when the key in the variable:

iex(4)> key = :b
:b
iex(5)> %Foo{a | key => 3}
** (CompileError) iex:5: unknown key key for struct Foo

The question: How to get new struct when I have key to update in a variable?

3

3 Answers

6
votes

Structs provide compile-time checks that the keys of the data are restricted to the values you specify. For this reason, it's not possible to use a dynamic key with struct semantics, because the value of the key is known only at runtime.

You can update the data, but you have to use map semantics and forego the compile-time checks:

%{foo | key => "bar"}

or

Map.replace!(foo, key, "bar")

These will give you a runtime error if the key is not valid.

This is why when using structs it's better to specify the keys at compile-time using struct semantics if possible.

1
votes

In Elixir, structs can only ever have the keys that you assigned to it in the defstruct macro.

iex(1)> defmodule Foo do
...(1)> defstruct [:a, :b]
...(1)> end
{:module, Foo,
 <<70, 79, 82, 49, 0, 0, 5, 184, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 180,
   0, 0, 0, 18, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 7, 99, 111, ...>>, %Foo{a: nil, b: nil}}
iex(2)> struct = %Foo{a: :a}
%Foo{a: :a, b: nil}
iex(3)> %{struct | b: :b}
%Foo{a: :a, b: :b}
iex(4)> %{struct | something: :b}
** (KeyError) key :something not found in: %Foo{a: :a, b: nil}
    (stdlib) :maps.update(:something, :b, %Foo{a: :a, b: nil})
    (stdlib) erl_eval.erl:259: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3
iex(4)> key = :b
:b
iex(5)> %{struct | key => :c}
%Foo{a: :a, b: :c}
iex(6)> key = :different
:different
iex(7)> %{struct | key => :d}
** (KeyError) key :different not found in: %Foo{a: :a, b: nil}
    (stdlib) :maps.update(:different, :d, %Foo{a: :a, b: nil})
    (stdlib) erl_eval.erl:259: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3

You can use %{struct | key => value} just fine as long as the value of key is one of the keys that struct has.

0
votes

You can use Kernel.struct/2 which lets you use Elixir's pipe operator. Using your example struct, you can use Kernel.struct/2 this way:

a = %Foo{a: 1, b: 2}
struct(a, a: 5) // outputs %Foo{a: 5, b: 2}

a = %Foo{a: 1, b: 2}
struct(unknown_key: "hello") // outputs %Foo{a: 1, b: 2}

a = %Foo{a: 1, b: 2}
a |> struct(a: 5) |> do_something_with_updated_struct()