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!