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

AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4663 61218 595
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
515 12931 101
New
wmnnd
Hey folks, I wanted to share with you a project I’ve been working on for a while: It’s an Open Source (AGPLv3) newsletter tool. Think Ma...
New
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
New
sntran
In my third year of college, around 2010, I found Erlang. A few years later, I found Elixir. I liked the ideas at once: many small proces...
New
gtcode
I’ve been tinkering with Lynx (by José) via hold_ledger. I used AI to write the code, a separate specification, the tests, and the Lean p...
New
Ganna
Hi everyone, I have 8 years of production experience with Erlang/OTP and I’m just starting to get to know Elixir. What would you learn f...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
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
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
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
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews