Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Structs

reading/structs.livemd

Structs

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue ModulesRock Paper Scissors Lizard Spock

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How do you use a struct to structure data?
  • Why might you use a struct vs a map?
  • How do you manipulate and access values in a struct?

Structs

We’ve learned how to abstract behavior in our programs, but what about data?

It’s often useful to be able to create a custom data structure. That’s what structs are for. Struct is simply a short word for structure. They are an extension on top of maps that enforce constraints on your data.

Defining A Struct

We use the defstruct keyword to define the allowed keys in our struct. Instances of the struct will not be allowed to contain any data other than these keys.

Here’s an example struct called StructName. The name StructName could be any valid module name. We define :key1, :key2, and :key3 keys for the struct. Keys in a struct should always be atoms, but they can be any valid atom.

defmodule StructName do
  defstruct [:key1, :key2, :key3]
end

You’ll notice that structs definitions are created inside of a module. We then use the module name to create an instance of the struct.

Keep in mind, the struct definition in the module is like the blueprint to a house. It contains the plans, but it’s not the actual physical building.

Similarly, the struct instance is the actual instance of the struct. It’s one instance of actual struct data as defined by the struct definition.

We can create an instance of as struct using %StructName{} syntax. This looks similar to a map, because structs are actually implemented using maps under the hood.

%StructName{}

You’ll notice that the struct has a name key, but no value since we didn’t provide anything.

Here’s how you can pass in a value for a given key.

%StructName{key1: "value 1"}

We can pass in values for any or all of our keys.

%StructName{key1: "value 1", key2: "value 2", key2: "value 2"}

If we provide an invalid value, our struct instance will raise a KeyError.

%StructName{invalid_key: "invalid key value"}

This enforcement of data shape is why we might want to use a struct instead of a map, which does not enforce which keys must be present.

Default Values

By providing struct keys as a keyword list, we can define default values for the key.

defmodule DefaultKeys do
  defstruct key1: "default1", key2: "default2"
end

The key will have the default value if we don’t provide it to the instance of our struct.

%DefaultKeys{}

Or we can override the default value like so.

%DefaultKeys{key1: "OVERRIDE!"}

Structs can have keys with and without a default value.

defmodule SomeDefaults do
  defstruct [:key2, key1: "default"]
end
%SomeDefaults{}

Default keys must come last in the list of struct keys otherwise Elixir will raise a SyntaxError.

defmodule BadDefaults do
  defstruct [key1: "default", :key2]
end

Enforce Keys

It’s common to validate data in a struct. For example, you can use the @enforce_keys module attribute to enforce that certain keys are set.

defmodule EnforcedNamePerson do
  @enforce_keys [:name]
  defstruct [:name]
end

Creating an instance of EnforcedNamePerson without passing the enforced :name key a value will cause the struct instance to raise an error.

%EnforcedNamePerson{}

To avoid repetition, we can use the @enforce_keys module attribute in the defstruct definition, and add any non-enforced keys using ++.

defmodule EnforcedNamePersonWithAge do
  @enforce_keys [:name]
  defstruct @enforce_keys ++ [:age]
end

Your Turn

Define a Coordinate struct which must have :x and :y keys. Enforce these keys.

Example solution

defmodule Coordinate do
  @enforce_keys [:x, :y]
  defstruct @enforce_keys
end

Enter your solution below.

defmodule Coordinate do
  @enforce_keys [:x, :y]
  defstruct @enforce_keys
end
%Coordinate{x: 0, y: 5}

Module Functions

A module that defines a struct can contain functions just like a normal module.

defmodule Person do
  defstruct [:name]

  def greet(person) do
    "Hello, #{person.name}."
  end
end
person = %Person{name: "Peter"}

Person.greet(person)

Your Turn

  • Define a new struct Hero.
  • A Hero will have a :name and :secret_identity.
hero = %Hero{
  name: "Spider-Man",
  secret_identity: "Peter Parker"
}
  • Create a Hero.greeting/1 function which uses the hero struct instance and return a greeting.
Hero.greeting(hero) 
"I am Spider-Man."
  • Create a Hero.reveal/1 function which accepts the hero struct instance and reveals the hero’s secret identity.
Hero.reveal(hero) 
"I am Peter Parker."

Example solution

defmodule Hero do
  defstruct [:name, :secret_identity]

  def greeting(hero) do
    "I am #{hero.name}."
  end

  def reveal(hero) do
    "I am #{hero.secret_identity}."
  end
end

Enter your solution below.

defmodule Hero do
  @enforce_keys [:name, :secret_identity]
  defstruct @enforce_keys ++ [:super_power]

  def greeting(hero) do
    "I am #{hero.name}!"
  end

  def reveal(hero) do
    "I am #{hero.secret_identity}!"
  end

  def powers(hero) do
    "My power is #{hero.super_power}!"
  end
end

When finished, bind a variable hero to an instance of your Hero struct.

hero = %Hero{name: "The Invisible Man", secret_identity: "Redd Burns", super_power: "Invisibility"}

Use the Hero.greeting/1 function on hero to ensure it works correctly.

hero |> Hero.greeting

Use the Hero.powers/1 function on hero to ensure it works correctly.

hero |> Hero.powers

Use the Hero.reveal/1 function on hero to ensure it works correctly.

hero |> Hero.reveal

Manipulating Structs

Structs are an extension of maps under the hood, so you can use the same map update syntax.

defmodule MyStruct do
  defstruct [:key]
end
initial = %MyStruct{key: "value"}
updated = %{initial | key: "new value"}

You can also access values using dot notation.

instance = %MyStruct{key: "value"}

instance.key

However, we can’t use square bracket notation. That makes sense, since a struct instance should always have the keys the struct definition defines.

instance[:key]

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Structs reading"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue ModulesRock Paper Scissors Lizard Spock