brettp

brettp

We ran into an issue today with structs that I found a bit surprising.

The essence of the issue is:

  • A struct is a Map under the hood
  • you can manipulate a struct with Map functions, deleting keys that should be there and adding keys that shouldn’t
  • as long as the __struct__ key is still there, the Map is treated as a struct including in Elixir pattern matching

Here’s some code which demonstrates this:

defmodule Sandbox do    
  @enforce_keys [:mandatory]
  defstruct [:mandatory, :optional]

  def struct_foo do
    s1 = %Sandbox{mandatory: "must have this"}
    do_something("s1", s1)

    s2 = Map.delete(s1, :mandatory)
    do_something("s2", s2)

    s3 = Map.put(s1, :bogus, "blah")
    do_something("s3", s3)


  end

  def do_something(s, %Sandbox{} = thing) do
    IO.puts "#{s} #{inspect thing} is a %Sandbox"
  end

  def do_something(s, thing) do
    IO.puts "#{s} #{inspect thing} is just a thing"
  end

end
iex(24)> Sandbox.struct_foo()
s1 %Sandbox{mandatory: "must have this", optional: nil} is a %Sandbox
s2 %{__struct__: Sandbox, optional: nil} is a %Sandbox
s3 %{__struct__: Sandbox, bogus: "blah", mandatory: "must have this", optional: nil} is a %Sandbox

One interesting thing is that IO.inspect seems to know the difference between the original struct and one that’s been hacked into a Map, but Elixir pattern matching doesn’t.

So you almost certainly should not be manipulating a struct in this way, and you certainly shouldn’t be deleting keys from structs. The question is: should (could) Elixir do anything to stop you doing this? Should Map functions check for __struct__ and behave differently if they are asked to do something nefarious to a struct? Should a Map function which manipulates a struct at the very least also remove the __struct__ key so it returns a Map that is not treated as a struct any more?

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

If you know you’re working with a struct and want to preserve its integrity use struct/2 or struct!/2.

dimitarvp

dimitarvp

That’s a really weird code though. It’s known that you shouldn’t try adding/deleting keys to/from a struct.

Why would you have code like your above?

brettp

brettp OP

Well I don’t have code like that – that’s just sample code to demonstrate the underlying behaviour: that Elixir allows you to manipulate a struct into something that looks nothing like the struct, but it still treats it as a struct.

(For the curious, the issue was that somewhere in a codebase someone had, with good intentions, deleted from a struct all keys whose value was nil. This caused issues elsewhere in the codebase – specifically when relying on the default @derive implementation of Jason.Encoder – where code assumed that if something was a struct then all the keys should be there.)

I know you shouldn’t write code like that; my question was whether Elixir should - or indeed could - be doing something to stop you.

dimitarvp

dimitarvp

So if you are dealing with a larger code base and more developers then I’d say it makes sense to convert the strict structs to loose maps on their way out of the code you control. Let the other modules / projects do whatever with them and then try and convert them back to strict structs via the struct function on their way in back to your code.

OvermindDL1

OvermindDL1

I’m surprised dialyzir didn’t catch it, this seems like a case it would catch?

jeremyjh

jeremyjh

If the type is not opaque what could dialyzer catch here? It is not even aware of structs. Making a struct opaque is possible but then you have to write accessors for every piece of data you might want to use.

OvermindDL1

OvermindDL1

But dialyzer does know that access on a struct’s/map’s fields that don’t exist is an error.

jeremyjh

jeremyjh

I don’t think any error will be raised in the above code. The Map.put option succeeds, so what is there for dialyzer to complain about? The elixir compiler does complain about access or pattern match of non-existing fields, but dialyzer will never even see that code. It should catch cases that must lead to an error in exactly the same circumstances that it would for a bare map, but no others unless user-defined types are supplied.

brettp

brettp OP

One of the things I find odd is that IO.inspect somehow knows the difference between a struct and the Map with a __struct__ key that is returned from e.g. Map.delete:

struct: %Sandbox{mandatory: "must have this", optional: nil} is a %Sandbox
map with a __struct__ key: %{__struct__: Sandbox, optional: nil} is a %Sandbox

josevalim

josevalim

Creator of Elixir

Correct.

It could but it would make all maps operations more expensive, which is mostly why we don’t. If we had a static type system, doing Map.put/3 on a struct would certainly fail.

Where Next? Top

Trending in Discussions Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
jtormey
Lately I’ve been thinking about how to organize components as a LiveView application grows. One of the pain points I’ve found (for myself...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
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
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews