← All series

Series contents

Log in to track your progress

  1. 1. Welcome to Alchemist Camp Episode
  2. 2. Lesson 1: The guessing game Episode
  3. 3. Solution 1: The Greeter Episode
  4. 4. Lesson 2: Word Count Episode
  5. 5. Word Count ULTIMATE EDITION! Episode
  6. 6. Minimal Todo List Part 1 Episode
  7. 7. Elixir lists vs Elixir tuples Article
  8. 8. The Image Snatcher Episode
  9. 9. Minimal Todo List Part 2 Episode
  10. 10. Elixir maps made effortless Article
  11. 11. Fibonacci with tail recursion Episode
  12. 12. Mini Markdown Episode
  13. 13. Extending Mini Markdown Episode
  14. 14. Alchemy Markdown Episode
  15. 15. Using mix to modularize and add tests Episode
  16. 16. Command line applications with escript Episode
  17. 17. Caboose, a simple Unix tail clone Episode
  18. 18. Game board (Tictac Part 1) Episode
  19. 19. Structs: Maps with checks and default values Article
  20. 20. Deck generation, Pythagorean Triplets and more Episode
  21. 21. Whiteboarding the Tictac state machine Episode
  22. 22. Game state machine (Tictac Part 2) Episode
  23. 23. Modelling the memory game Episode
  24. 24. Finishing Tictac and making a CLI Episode
  25. 25. Threatened by rooks, bishops and knights Episode
  26. 26. A worker to log stats from a YouTube’s API Episode
  27. 27. Constructing bitmap files with Elixir Episode
  28. 28. Making bitmaps with color palettes Episode
  29. 29. Flattening lists recursively in Elixir Episode

Structs: Maps with checks and default values

After our crash course, Elixir maps made effortless, a 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!

Open standalone page ↗

No Comments