josevalim

josevalim

Creator of Elixir

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

Showing Posts 72 to 63

mortenlund

mortenlund

Just tested the latest version and this works! This feels like a great pattern.

I am encouraged to pattern match which is perfect and I also get to prove to my future-self that I actually intended to update the particular struct!

josevalim

josevalim OP

Creator of Elixir

I believe I forgot to update here but the latest version of Elixir which treats this as a type check is working really well and we don’t have plans for now to deprecate it. I believe the warning was also updated to say you might keep it or remove it. As you prefer!

mortenlund

mortenlund

Hi!

Been testing the “new” syntax for a while now and I definitely feel like I am loosing some sense of overview.
Using the %Struct{var | key: value} gave me immediate information about what was going to happen.

A simple example would be something like this

“Old way”

def myfunc(%Struct1{} = arg1, %Struct2{} = arg2, %Struct3{} = arg3) do
  {%Struct1{arg1 | par1: :value1}, 
   %Struct2{arg2 | par2: :value2}, 
   %Struct3{arg3 | par3: :value3}}
end

Compared to the new way:

def myfunc(%Struct1{} = arg1, %Struct2{} = arg2, %Struct3{} = arg3) do
  {%{arg1 | par1: :value1}, 
   %{arg2 | par2: :value2}, 
   %{arg3 | par3: :value3}}
end

Just visually you loose the ability to see what struct is being updated and you have to follow the variable names backwards to infer the type, and more importantly you loose the visual aid, specially with syntax highlighting, that you are actually updating a struct and not any other map.

I would also imagine that this is also a harder problem for a LSP to figure out which struct it could provide auto-completion for as well?

In my opinion I would gladly wait couple more milliseconds/seconds for the compiler to verify the correctness of my keys if that is the downside of this approach.

Also, if the LSP together with the IDE could visually present the inferred type that would also satisfy my needs in this regard.

If the type checker would allow the usage of Struct update syntax if the variable used is already pattern matched that would also be great!

tfwright

tfwright

Does this mean the warning reported here and the explanation of it is accurate? Warning about missing pattern match, despite pattern matching - #11 by garrison

i.e. using the old update syntax will generate a warning regardless of pattern matching? Or is there a pattern match error there those of us in that thread are missing? Because if it’s the former I will do as it sounds like others have done and deprecate that syntax now before I even try to update to 1.19

GrammAcc

GrammAcc

Actually, thinking about the formatter fix a bit more, this might work:

%User{user | name: ""}

to

%{%User{} = user | name: ""}

Which feels cursed to me, but would probably be easy to grep for to update later on?

GrammAcc

GrammAcc

That fix would be safe for any uses of the struct update syntax that aren’t generating the warning about a missing match, but it seems like an automatic fix for the type safety warning would be really hard. There are a lot of different ways that the variable could have been declared, and adding pattern matching can easily cause a line to exceed the line limit moving all the other code around, which might not be automatically reversible with the formatter, so this is one of the situations where Elixir’s one-way formatter changes will bite hard.

The most reliable fix would probably be to rebind the user variable right above the update line like:

%User{} = user
%{user | name: ""}

But that would probably piss me off if I saw it all over my code. Seems like manual intervention is the best approach. Vim macros to the rescue again. :slight_smile:

That being said, this would be really painful for a warnings-as-errors toolchain. @josevalim I found this old thread about disabling specific warnings https://groups.google.com/g/elixir-lang-core/c/QD3uG7WxZlM/m/Qh-CbD6fAAAJ, which would resolve the pain point most people in this thread have expressed around this update. Even if the team fixed all the warnings in their own code, having hundreds of these warnings from deps that might require devops support and a security review to update in a large org would be really painful. Is it still a firm “no” from the core team to be able to suppress specific warnings in the compiler flags?

GrammAcc

GrammAcc

Awesome! Thanks for confirming and for putting in the work!

Asd

Asd

Styler has added a semi-manual solution, where it translates the %User{user | name: ""} to %{user | name: ""}, but it does not add the matching in the arguments, which, if not followed up by manual changes, makes the code worse than it used to be.

josevalim

josevalim OP

Creator of Elixir

The RC emitted a different warning. So check latest. I also think styler may have added an automatic fix for this.

Asd

Asd

Do you plan to add formatter or any other fixup tool to automatically fix it? Cause I’ve just tried 1.19-rc and I got many many many screens of warnings in every dependency and the host project. This looks like a lot of manual work for someone who has pipelines with --warnings-as-errors

Where Next? Top

Trending in Notices Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews