DEV Community

Mark
Mark

Posted on • Originally published at alchemist.camp

2 2

Elixir Structs are maps with checks and default values

After our crash course, Elixir maps made effortless, the next logical building block is structs.

What are Elixir Structs?

As explained in the official docs, Structs are extensions built on top of maps that provide compile-time checks and default values. Maps, as we covered, are one of Elixir's basic data structures. They're the primary way we store key-value pairs:

> users = %{
  "sam" => %{age: 22},
  "pat" => %{age: 58}
}

> user1 = Map.get(users, "sam")
%{age: 22}

> Map.get(user1, :age)
22

# Or in one step...
> get_in(users, ["pat", :age])
58

Aside from getting values from maps via standard library functions like Map.get/3 or Kernel.get_in/2, there are also two built-in syntaxes:

  • The dot syntax, which throws errors on missing keys
> user1.weight
** (KeyError) key :weight not found in: %{age: 22}
  • The [] syntax, which is forgiving:
> users[:nobody]
nil

Note that the dot syntax assumes keys are atoms and if keys are Strings as above, only the bracket syntax can be used. For more on working with maps, see: Elixir maps made effortless

Structs are built on top of maps

Let's try pasting a simple module with a defstruct into iex and then poking around at it:

defmodule User do
  defstruct age: :nil, name: "anonymous"
end

> user1 = %User{age: 32}
%User{age: 32, name: "anonymous"}

> user2 = %User{}
%User{age: nil, name: "anonymous"}

Using a struct makes it possible to define default values. It also lets us be confident that any User we create will at least have the two keys of age and name. It also makes it possible for us to match various kinds of structs in a case statement, like this:

case user1 do
  %User{} -> IO.puts("This is a user")
  %Admin{} -> IO.puts("This is an admin")
  _ -> IO.puts("I don't know what this is")
end

This will do a compile-time check to ensure both the structs are defined and then it will check the hidden __struct__ key of user1 to see which kind of struct it is. If the Admin struct isn't defined, then you'll see this:

Admin.__struct__/0 is undefined, cannot expand struct Admin

Enforcing keys

We can go a step further. By using @enforce_keys at the top of a struct module, we can enforce that a specific set of keys are used when creating a struct

defmodule User do
  @enforce_keys [:age, :name]
  defstruct age: :nil, name: "anonymous", favorite_color: "purple"
end

Then, we'll see the following in iex:

> bob = %User{}
(ArgumentError) the following keys must also be given when building struct User: [:age, :name]
> bob = %User{name: "Bob", age: 48, favorite_food: "steak"}
** (KeyError) key :favorite_food not found
> bob = %User{name: "Bob", age: "48"}
%User{age: 48, favorite_color: "purple", name: "Bob"}

Now, users must have names and ages, but favorite colors are optional. Any other key is invalid. Again, this is enforced at compile-time, so it is possible to patch together a struct that violates the specified behavior by directly setting the __struct__ field rather than using the %User{} syntax. This isn't a good idea to abuse, but it can be useful in some situations when building libraries.

Here's how we could create a "user" who's missing a required key and includes a key that isn't part of the %User{} struct:

> evil_bob = %{__struct__: User, name: "Bob", favorite_food: "steak"}
> case evil_bob do  
>   %User{} -> IO.puts("This is a user")
>   _ -> IO.puts("This isn't a user")
> end
This is a user
:ok

😱

A hands-on example

If you want to dig into a simple, plain Elixir project using Structs, take a look at the Tic-tac-toe game board screencast!

Request a free email-based Elixir course from Alchemist.Camp

Image of Datadog

The Future of AI, LLMs, and Observability on Google Cloud

Datadog sat down with Google’s Director of AI to discuss the current and future states of AI, ML, and LLMs on Google Cloud. Discover 7 key insights for technical leaders, covering everything from upskilling teams to observability best practices

Learn More

Top comments (0)

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay