josevalim

josevalim

Creator of Elixir

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
0 voters

Most Liked

NobbZ

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…

28
Post #6
christhekeele

christhekeele

IMO I disagree that this reduces the surface area of Elixir, it increases it. As long as these are true:

  1. Structs are Maps
  2. Maps have an update syntax: %{var | ...pairs}
  3. 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

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.

16
Post #7

Where Next?

Popular in Notices Top

josevalim
Hello everyone, We had a discussion on elixir-lang-core about deprecating the use of ‘single quotes’ in Elixir for charlists. The use o...
New
josevalim
Hello everyone, Last year we have announced GenStage and then developed Flow. Our initial plan was to make GenStage part of Elixir. Tha...
New
josevalim
Hi everyone, With the v1.7 release, we have received reports of some dependencies no longer working properly on the new release. Develo...
New
ericmj
We are moving the builds for precompiled versions of Elixir, Erlang/OTP, and Hex itself. These are currently hosted on https://repo.hex.p...
New
josevalim
One common concern about using Elixir libraries is that use SomeModule does not make clear how it impacts the caller. While the goal of u...
New
josevalim
Hi everyone, I have updated Elixir’s wiki page on IDE/Editor support and I am now linking to it from the website. If you know of other ...
New
josevalim
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 E...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New

We're in Beta

About us Mission Statement