Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum.
In the (beta version of the) book, they update structs using Map.put/3. I’ve found myself doing the same in my own code. @hubertlepicki astutely observed that this opens up the opportunity for mistyped field names when updating your structs: This is totally allowed, the result of Map.put/3 will then be a map (instead of an instance of your struct).
Example:
iex> defmodule Foo do
iex> defstruct bar: 1 , baz: 2
iex> end
iex> foo = %Foo{}
iex> foo |> Map.put(:qux, 4)
%{__struct__: Foo, bar: 1, baz: 2, qux: 4} # <- note that it is printed as a map.
The other two methods known to me to update structs are:
The special struct update syntax, %Foo{oldfoo | bar: 4}. This will result in a compile-time error when wrong field names are used. Drawback: It cannot be piped.
put_in/2: newfoo = put_in(foo.bar, 4). This will also result in a compile-time error when a wrong field name is used. Drawback: It, too, cannot be piped.
put_in/3 (as in: newfoo = put_in(foo, [:bar], 4)would be able to be piped, but it is not available for structs; it only works on things that implement the Access protocol.
So what is the best way to update structs in practice? Am I missing any method in this list? Would it be a good idea to have a version of put_in that works on structs and is pipeable?
Yes, I raised the issue because I did waste some time on a stupid typo. Generally when I use struct, I want to have only defined fields. Otherwise I’d go with a Map.
But if you do use Map.put/3 on a struct, it works, yet not properly since I mistyped/used wrong key.
I agree that not being able to do it in pipe is not ideal. I found myself wrapping the update syntax in a tiny function/anonymous or othwerwise that does that for the very reason. I don’t know, maybe there is better way somehow so I’m listening too.
Erlang has :maps.update/3, which is basically the %{x | key => value} syntax as a function. I think having something similar in elixir would be beneficial. The biggest problem is the name.
Yes, Map.update! is fine - if only you didn’t have to create a useless anonymous function. It’s awesome in many cases that Map.update! accepts a function, but not for this case.
Would it be an idea then to add a version of Map.update! that does not require you to use a function as last parameter?
Although of course, having both a version that runs a function and a version that puts a value at the key regardless of what it is would mean that functions are basically treated ‘special’, which might be confusing.
There already is another module that has only exactly two functions: Atom, so it does not feel that weird to me to add a new module.
Another approach that might be possible, is to make sure that passing the struct update syntax: foo |> %Foo{bar: 1} would work. I think this could be done by altering the current clauses of the pipeline operator.
the syntax inside the access sigil - what is it? Is it your custom invention?
I am asking because I am using something similar yet different in one of my proejcts where I do need to access nested data coming over from json - I did something that resemblex xpath very much instead.
Can you explain the scenario where this would not work? Like I mentioned earlier I’m new to the language but in the original poster’s scenario they simply want to update an existing struct’s fields only. My solution includes the ability of piping e.g.
iex> defmodule Foo do
iex> defstruct bar: 1 , baz: 2
iex> end
iex> foo = %Foo{}
iex> foo |> struct(%{bar: 3, qux: 4)
%Foo{bar: 3, baz: 2} # only existing struct field is updated
Interesting! I did not know about Kernel.struct/2. This seems to work (not being able to insert non-existing fields) and is pipeable. It does have the behaviour of silently disregarding wrong fields rather than raising a (compile-time) error when a wrong field is accessed, which means that the problem of typographical errors in struct field names is not taken care of. I foresee a lot of hard-to-track-down bugs because of this .
@josevalimMap.put_existing sounds like a great idea to me.
@bopjesvla I did not at all think about Axe’s access sigil while creating this topic. While Axe seems like it solves this problem rather elegantly, I think an access sigil is too heavy a feature to be able to be added to the core language.