Introducing the Struct

A struct is an extension of a map with more strict rules about what keys it can have. A struct’s keys are atoms and cannot be strings. Because of the strict definition of a struct, Elixir can provide compile-time checks for the keys. This won’t prevent you from adding invalid keys to the map, but it works very well for catching typos and when a key is renamed.

A struct is defined inside a module and the name of the struct is the module itself.

A simple struct:

defmodule Player do

  defstruct [:username, :email, :score]

end

This defines a Player struct where the data has 3 named attributes. For a new struct, all the attributes will have the default value of nil. When you copy/paste that module definition into IEx, you can then auto-complete on “Player.” and it completes to Player.__struct__. Execute that and you see the struct.

Player.__struct__
#=> %Player{email: nil, score: nil, username: nil}

The struct gets the name of the module. Since we didn’t define any default values for the struct, Elixir assigned nil to each of the attributes. For a Player, it would make sense that the score should default to 0 instead of nil.

Default Values

When declaring the struct, we can provide a keyword list to provide default values. That looks like this:

defmodule Player do

  defstruct username: nil, email: nil, score: 0

end

When creating a new Player struct, it defaults the score to 0 for us.

%Player{}
#=> %Player{email: nil, score: 0, username: nil}

Compile Time Checks on Keys

As mentioned before, Elixir can perform compile time checks for valid keys on the struct. This is an example of a runtime error for an invalid key.

%Player{lives: 100}
#=> ** (KeyError) key :lives not found
#=>     expanding struct: Player.__struct__/1

A Struct is a Map

A struct is a map and can be accessed using normal map functions.

gary = %Player{username: "Gary", score: 100}
#=> %Player{email: nil, score: 100, username: "Gary"}

is_map(gary)
#=> true

Map.get(gary, :score)      
#=> 100

gary.score
#=> 100
Thinking Tip: A struct can be like an OOP class

An Elixir struct is similar to a “class” in Object Oriented Programming languages. If you think about a class as the explicit linking of a data structure and the methods (or functions) that operate on that data, then a well-defined struct/module can do the same thing. The main difference for a struct is that the data structure and the functions are not explicitly tied together. We define them in the same place as a convenience both to the developer creating the data structure and writing the tests, but also to the developer using the struct. The primary functions for operating on the struct are located in the same namespace.

No Default Access Behaviour

A struct does not implement the Access Behaviour mentioned when talking about Maps. That behaviour (yes, spelled the British English way) allows you to use [] to provide a key to access a value in a map. When you try that on a struct it fails.

gary[:username]
#=> ** (UndefinedFunctionError) function Player.fetch/2 is undefined (Player does not implement the Access behaviour)
#=>     Player.fetch(%Player{email: nil, score: 100, username: "Gary"}, :username)
#=>     (elixir) lib/access.ex:322: Access.get/3

gary.username  
#=> "Gary"

Structs have pre-defined atom keys. You can use the key name like gary.username and it returns the value.

There’s Much More to Structs

This is only an introduction to Elixir structs so we have enough of a foundation to use them in pattern matching. The combination of structs and pattern matching are powerful and beneficial. They are two features that go great together.

For more information on structs, they are covered in more detail in the Elixir Fundamentals Collection. You can also find information online.

Comments are closed

This is a static version of the site. Comments are not available.

4 Comments

  1. Larry Rix on October 28, 2022 at 6:39 am

    For me—this really warrants a larger discussion (but not here). While the error message makes an emphatic imperative declaration about the syntax and grammar of the “defstruct” construct, such choices ought never be seen as though they are the only ones available—that is—Elixir could have been designed differently. Some immediate questions are: First, why not more than one struct per module? Second, why not use Defstruct instead of Defmodule? Third, why not use the “do … end” construct with “defstruct”?

    In the end—all language design choices have consequences—some good and some bad. Those choices can either help productivity or hurt it. One way to hurt programmer productivity is to create language constructs, syntaxes and grammars that hide bugs and help the programmer produce them. This leads to downstream issues with readability, maintainability, and overall software quality for the entire lifecycle of the software system.

    As a new Elixir/Phoenix programmer, such questions are fresh in my mind and seeing this error causes me to ask a lot of “why?” questions. 🙂

    iex(11)> recompile
    Compiling 1 file (.ex)

    == Compilation error in file lib/junk.ex ==
    ** (ArgumentError) defstruct has already been called for Junk, defstruct can only be called once per module

  2. Larry Rix on October 28, 2022 at 7:28 am

    Little things that are bothersome:

    1 def player_stuff do
    2 frank = %Junk{username: “Frank”, email: “frank@furt.dog”}
    3 Map.get(frank, :username, “no-name”)
    4 end

    The user of “username:” in line 2 and then the same reference in line 3 as “:username”.

    From a language design point of view, there is no reason that the parser/compiler cannot use “:username” as an atom. I know, I know, I know—you just have to “get used to thinking that way” in Elixir. But—the truth is this—the choices of the language designer are what force the programmer to “get used to it”.

    Am I being critical (perhaps unduly so)? Yes—I am. Does that mean I do not “like” Elixir? Not at all. It is a tool and a powerful one at that. Pattern matching is an outstanding invention and I wish many languages had it inherently in the syntax and grammar of its constructs.

    All in all—I am VERY thankful for this course material. It is outstandingly helpful!

    • Mark Ericksen on October 31, 2022 at 8:04 am

      You do have the option of writing it this way if you find it helpful:

      def player_stuff do
        frank = %Junk{username: “Frank”, email: “frank@furt.dog”}
        frank.username || “no-name”
      end
      

      However, if the map doesn’t contain an atom key named :username then it will result in a runtime error. But when working with a struct that defines the field, it will always be present.

  3. butcher on November 21, 2024 at 12:43 pm

    | That behaviour (yes, spelled the British English way) …

    Finally, I found a soul mate in this mad universe.

Comments are closed on this static version of the site.