josevalim
Changes to the struct update syntax
Hi everyone,
Elixir v1.19.0-rc.0 included a deprecation of the struct update syntax. However, we will ship with a different warning on Elixir v1.19.0-rc.1. We have a poll at the end that will help us decide the final behaviour.
Deprecation of the struct update syntax in Elixir v1.19.0-rc.0
Elixir v1.19.0-rc.0 deprecated the struct update syntax. The struct update syntax was introduced to help us find bugs in the field name:
def set_address_to_earth(user) do
%User{user | adress: "Earth"}
end
The above would check, at compile-time, that “adress” is a valid field and fail to compile in this case.
However, thanks to the type system, we no longer need the struct update syntax, by simply pattern matching on the %User{} struct, we already get similar guarantees:
def set_address_to_earth(%User{} = user) do
%{user | adress: "Earth"}
end
Not only that, pattern matching helps us find other bugs in the code too. For example, imagine you have this code:
def trim_address(user) do
%User{user | address: String.trim(user.adress)}
end
In this case, there is a typo when reading the field name, which is not caught by the update syntax, but it would have been caught if you pattern matched on the %User{} struct.
For those reasons, we have decided to deprecate the struct update syntax on v1.19.0-rc.0, and ask developers to use pattern matching instead. Especially because we noticed that several projects were using the update syntax instead of the superior pattern matching:
iex(2)> %URI{uri | path: "/"}
warning: the struct update syntax is deprecated:
%URI{uri | path: "/"}
Instead prefer to pattern match on structs when the variable is first defined and use the regular map update syntax:
%{uri | path: "/"}
└─ iex:2
Converting the struct update syntax into type assertions in Elixir v1.19.0-rc.1
Once we released v1.19.0-rc.0, we started hearing some concerns about removing the struct update syntax. In a nutshell, it was pointed out that while the struct update syntax is suboptimal for the type system, it may provide an important hint for readers of the code. Therefore a new proposal was introduced: Elixir should warn if you use the struct update syntax without pattern matching on the struct. In other words, if you wrote:
def trim_address(user) do
%User{user | address: String.trim(user.adress)}
end
Elixir will emit a warning, suggesting you to pattern on %User{}. Therefore the correct version would be this:
def trim_address(user = %User{}) do
%User{user | address: String.trim(user.adress)}
end
Of course, once you add the pattern matching, you may convert the struct update into a regular map update, if you desire to:
def trim_address(user = %User{}) do
%{user | address: String.trim(user.adress)}
end
Note that Elixir will only emit this warning if it cannot provide at compile-time it is a struct of certain type, working effectively as a type assertion.
The warning has been implemented in the v1.19 branch and it helped confirm that indeed, in many situations, only struct updates were used, and not pattern matching. For example, in Plug, we got a few warnings, such as this one:
warning: a struct for Plug.Conn is expected on struct update:
%Plug.Conn{conn | params: params}
but got type:
dynamic()
where "conn" was given the type:
# type: dynamic()
# from: lib/phoenix/controller.ex:1326:20
conn
when defining the variable "conn", you must also pattern match on "%Plug.Conn{}".
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:
user = some_fun()
%User{user | name: "John Doe"}
it is enough to write:
%User{} = user = some_fun()
%{user | name: "John Doe"}
typing violation found at:
└─ lib/phoenix/controller.ex:1334:5: Phoenix.Controller.scrub_params/2
To deprecate or not to deprecate, that is the question
While I believe the above is an improvement to Elixir v1.19.0-rc.0, as it forces people to pattern match before we potentially deprecate struct updates, we still need to decide if we want to deprecate the struct update syntax in v1.20 or later.
The argument to deprecate the struct update syntax is to reduce the language surface. If the struct update syntax must only be used alongside pattern matching, it effectively does not add any new guarantees to the language, and therefore there is an argument that it is no longer a useful construct.
The argument against deprecating it is that, while not useful to the compiler, it can be useful for humans. The main argument is that, if you have a long function, %User{user | ...} is a reminder and a type assertion of the type of the struct, while %{user | ...} would require you to keep more context in your head.
Therefore, I am reaching out to the community. The goal is to run a non-binding poll now, gathering community feedback, and then do another non-binding poll before v1.20, after people have migrated to v1.19, to understand how they have modified their codebases in practice. Thank you for participating!
Should we deprecate or keep the struct update syntax?
- Deprecate the struct update syntax in favor of pattern matching and map updates
- Keep the struct update syntax as a type assertion
Most Liked
NobbZ
For now I strictly prefer the struct update syntax, as I get field completion there.
And unless the LSPs will provide that completion for map updates after pattern match from the first day, I don’t see me using the “new” idioms in near time…
christhekeele
IMO I disagree that this reduces the surface area of Elixir, it increases it. As long as these are true:
- Structs are Maps
- Maps have an update syntax:
%{var | ...pairs} - Structs have a compiler-verification syntax:
%module_name{...pairs}
Then I would expect rules 2 and 3 to compose: such that structs can use both update and compiler-verification syntaxes together (today, %module_name{var | ...pairs}). That is, it is a syntax that emerges logically and consistently from simpler syntax rules.
Having to remember that rules 2 and 3 are not compatible would add to the language surface area by requiring memorizing that as a new rule, even if the rule itself is not additive, but restrictive. As a new special-case to memorize in addition to the above 3 rules, it increases the surface area of the language.
From the compiler’s perspective, I can understand how it feels like a redundant construct, as it is a form that must be handled discretely. But to programmers and especially newcomers, I’d argue that it’s not a construct at all in their heads, simply the co-location of two simpler constructs, and therefore does not make sense to deprecate “as its own” syntactic form, even if the type system allows expressing the same checks in other ways.
nicklayb
I’m personally in favor of keeping the syntax. My main reasons are from a human code reading perspective and the way i like to structure my code. (A lot of these are personal opinions)
For instance, to me, I like it when the code has a clear distinction between Map and Struct (I know that structs are maps in the end but I think from a code organization point of view, if we provide a way to define concrete structures, it makes sense to have them distinguishable from maps).
I see this being exactly like how Erlang works with Records. Records are just tuples in the end, but it makes sense to have the #User{email = email} syntax in order to provide the distinction between regular tuples.
Where I see the differences
Map:
- Can have whatever key in it
- Is used as a variable and extensible data structure
- Updated and accessed using
Map.*functions.
Struct:
- Static map, fields should never be added nor removed.
- Is used to represent a concrete, reusable data structure.
- Provide compile time safety on field presence
- Updated and accessed through operators (
|for updating and.for accessing).
From a code reading perspective, seeing Map.put(data, :key, value) gives me clear indication that data is a Map (not a struct). And seeing %URI{data | path: path} gives me a clear indication that I’m working on a URI struct. Seeing %{data | key: value} has ambiguity, it tells me "I’m updating something that is at least a map with an enforced :key key, is it a struct?, I cannot tell, I need to read the surrounding code. I agree though, that in a perfect world, functions are kept short where variables are referenced closed to other, but not everyone works the same.
(I’m not saying everyone should follow this philosophy but over time it has proven a huge maintainability factor in the way I write my applications).
I personally only see benefits in keeping it especially if it comes with a “You must pattern match beforehand” warning if you didn’t, this seems to me like the best of both worlds. It doesn’t force anyone to use if you don’t but if like me you like it, you also get a reminder to tell you, for case where you didn’t, to pattern match. I’d be more in favor of a credo rule for those who wanna apply this strictly
But my main question would be, what is the gain of completely deprecating it? I understand the idea to promote the pattern match but bringing the warning feels quite sufficient for me to solve that problem.
Popular in Notices
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








