5

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 Answers 3

9

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.

Sign up to request clarification or add additional context in comments.

Comments

2

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.

Comments

1

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()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.