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
8 Likes

I must say I am skeptical about how meaningful the struct update syntax is to address the readability problem. After all, if you have a long function, you likely have more than a handful of variables and you are unlikely to use the struct update syntax: we read struct fields more than we update them and best practices would suggest to encapsulate struct updates on the modules that define them, instead of scattering them around. Therefore, when dealing with long contexts, there are more general techniques one should rely on, such as using clear variable names and encapsulating logic in modules/functions.

PS: I kept this observation out of the post as it is based on my opinion (which may change).

13 Likes

Reasonable, but standalone not enough from DX experience. :icon_confused:

By default I prefer a flexibility in the code. However if there is a real value for a developer in exchange (as you have described) then it’s not just an another proposal, but a new good practice that’s better to be followed. :flexed_biceps:

Let’s see it by example … I may not agree with some credo checks and they can even give sometimes a false-positives, but after some time I’ve realised that my code have much better quality. In theory I have forced myself to change a style, but in practice I’m more proud of my code than before and I believe that the best argument that could be raised. :raised_hand:

So YES, please deprecate it! :+1:

Edit: Would there be any problem to ask for a patch release for db_connection and postgrex? I saw that you did that for ecto already:

v3.12.6 (2025-06-11)

Fix deprecations on Elixir v1.19.

1 Like

I’m not sure I even realized you could do %User{user | name: "name"} :sweat_smile: In the name of failing fast, forcing a pattern match makes far more sense in which case the struct update syntax just becomes redundant ā€œjust to be super duper extra safeā€ syntax.

1 Like

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…

19 Likes

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.

10 Likes

I just reread my post, and it feels rather demanding… I didn’t mean it that way. I am sorry for the sound of the message.

I prefer to not edit it though as there have been interactions already.

2 Likes

FWIW it didn’t come across as demanding to me. It was a solid point that it has the potential to interfere with useful tooling which is a jarring experience.

4 Likes

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.

12 Likes

I am really torn on that one but ultimately I voted to deprecate it. What tilted the balance was my own counter-counter-argument to this one:

…and, well, don’t have functions that are too long. :person_shrugging:

As for a more pure PL perspective, I say deprecate it but only if it tangibly simplifies the compiler and saves you work in the future.

2 Likes

I first was on the ā€œyeah, deprecateā€ side, because it would make the language smaller, But this post made me think; shouldn’t rather the %{} for structs be deprecated?

As a newbie, I would/could have expected that I get a proper map back from using the %{...} update syntax, i.e. converting a struct (which is a special map) into a proper map. I now wouldn’t being restricted by any struct limitations anymore and hence, could add arbitrary fields to it.

This

struct = ...
%{struct | new_field: value}

would then be equivalent to

struct = ...
map = Map.new(struct) 
%{map | new_field: value}

I know that this is not how it works, but it wouldn’t be unreasonable to assume this. Therefore keeping the %MySruct{..} syntax alive might be valuable, even though redundant for the compiler (we write code for humans not compilers :slight_smile: )

1 Like

After reading some more replies here I’m going to go sit on the fence.

7 Likes

Actually I don’t think this fully holds.

%{} create new map
%{var | key: 10} → create a new map, with all the same keys but key set to 10
%Mod{} → create new Mod

So then we’d get to: %Mod{var | key: 10}

which by the composition rules you’re describing should make a new Mod with all the keys in var except key set to 10.

i.e:

%Mod{} → %{__struct__: Mod}
%Mod{var | key: 10} → %{var | __struct__: Mod, key: 10}

In typing this out, I guess its debatable if
%{var | __struct__: Mod, key: 10}
would be the right interpretation, or
%{%{__struct__: Mod} | , key: 10} in which case you’d be right?

Not sure :sweat_smile:

2 Likes

In my mind it’s conceptually closer to the latter, but YMMV.

Less important, and I’m not sure I’m reading the actual implementation correctly, but if so it appears that

So closer to the latter than the former, but implemented specifically rather than emergent from expansion order of the two constructs.

1 Like

I think your point is salient FWIW. The fact that you can’t use them together might trip people up, but I do think there is still a subtle difference whereby the composition doesn’t necessarily do what it looks like it would do when combining the two. Perhaps a matter of perspective.

[a | b] is an example where the same symbol has totally different properties based on its surrounding context, for however much that adds to the conversation :laughing: I do think that the strategy of just requiring that the value match the type if you use struct update syntax is a really elegant solution to the stated type system related issues, to the point where the motivation to remove the syntax seems to have been handled. If it’s coming back down to preference given that the technical issue is solved, then there is no great reason to debate the issue, and IMO no significant reason to deprecate it (and I voted on the poll as such).

2 Likes

So in other words some dependencies or tools like LSP have to be updated to support new version of the language which is completely expected I guess … This is why they announce updates sooner and give release candidates, so the community would know more about it and decide if it’s the right time to make an update. :light_bulb:

In my case I’m already working on new release candidates mainly because what I commented above and because of optimisations. I see things are really going in a good direction and I’m very motivated to not stop them from making good things. :+1:

1 Like

I find myself in the camp that likes the explicitness of the struct update syntax. When I see such an expression, I just don’t have a question about what the thing is and I’m clear in the communication to other readers, too. By using the map update syntax, we weaken that more explicit expression. That’s my simplistic take on the matter.

This isn’t to say that the pattern matching warning is unwelcome or that I want to avoid the pattern match because we have the struct update syntax… just that if I have a struct I don’t want to lose the discipline of treating it as such everywhere I might reference it.

And no, if it is deprecated I won’t lose too much sleep.

One final note on deprecation. There are a number of libraries out there, some of them that get some use, but perhaps less maintenance. While in some sense they may be ā€œdoneā€ and not require a lot of attention, deprecations can cause a sudden requirement for attention which might not be readily available. I still use libraries which cause warnings about charlists and not using the sigil, though for all intents and purposes they continue to work fine, if noisily during compile. But these libraries aren’t looking like they’ll be updated anytime soon ether. That’s the only concern that I really have given some of the thinness of support for some these things.

3 Likes
defmodule MyStruct do
  @moduledoc "module documentation"

  defstruct [:my_field]

  @typedoc "field documentation"
  @type my_field :: …

  @typedoc "struct documentation"
  @type t :: %__MODULE__{
          my_field: my_field()
        }

  # could be also done with a single and simple reduce similarly to for statement below
  @type field :: {:my_field, my_field()} | …
  @type fields :: [field()]

  @doc "put/2 documentation"
  @spec put(t(), fields()) :: t()
  def put(struct, fields), do: struct(struct, fields)

  @doc "put/3 documentation"
  def put(struct, field_name, field_value)

  for field_name <- [:my_field] do
    var = Macro.var(field_name, __MODULE__)
    @spec put(t(), unquote(field_name), unquote(var)) :: t()
    def put(struct, unquote(field_name), unquote(var)) do
      Map.put(struct, unquote(field_name), unquote(var))
    end
  end
end

With this code:

  1. We can easily create a generic macro if we want to do same thing for many modules.
  2. We have ful LSP support, documentation and specification
  3. We no longer need struct update syntax
# old
%User{user | address: "Earth"}

# new
User.put(user, :address, "Earth")
# or
User.put(user, address: "Earth")

There should be no problem with readability, code completion and so on … We already have a good patterns that we can follow, so there is really no need for a struct update syntax.

1 Like

Just my ¢2.

I don’t have anything against %User{user | age: user.age + 1} syntax, but it feels counter-idiomatic to me beyond two main usecases: ā‘  User is somewhat ā€œmoreā€ than a structure and updates are nevertheless should pass through kinda validation (via Changesets in Ecto, or via Access in Estructura,) and ā‘” state management in GenServer callbacks. I personally very rare if never met floating-by structs being just updated in the wild.

Assuming that ā‘  is covered by the respective helpers (direct update with %_{_ | kv} is nevertheless should be avoided at any cost,) we have ā‘” left. Yesterday I managed to clean up my libraries from the not-yet-but-to-be-deprecated syntax and I can tell, one would not miss these endless %__MODULE__{state | key: :value} in {:reply, …} and {:noreply, …} clauses.

Theoretically I can relate to the opinion %User{… | kv} is more suggestive, but in real code in my experience it is not. 99% of cases are covered by ā‘  and ā‘” above, and in ā‘  there is no such thing as a direct struct update, while in ā‘” it’d be rather %__MODULE__{… | kv} than %State{… | kv}.

The 1% left should be IMHO covered by introducing helpers within the module declaring a struct, as José mentioned above.

That all being said, I am voting for less noise, specifically taking into account that inability to sweep the unclearance of the code under the rug of final ā€œboom! it’s still a user struct!ā€ would eventually but inevitably force us devs to write cleaner code avoiding updates of who-the-hell-knows-what-this-struct-is in 20 lines under it’s definition :slight_smile:

3 Likes

If as a result of the change, someone replaces this:

def update(foo) do
  %Foo{foo | bar: "baz"}
end

with

def update(foo) do
  %{foo | bar: "baz"}
end

is there a way to realise that foo is always a Foo and warn then that writing

def update(%Foo{} = foo) do
  %{foo | bar: "baz"}
end

would be better? I think this is the main thing that would be lost by deprecating the syntax, if the result is code that might cause bugs by performing operations on any map/struct that happens to have the right keys, when the intention was only ever to operate on a specific struct.

4 Likes