Skip to content

Latest commit

 

History

History
323 lines (220 loc) · 7.26 KB

structs.livemd

File metadata and controls

323 lines (220 loc) · 7.26 KB

Structs

Mix.install([
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:tested_cell, github: "brooklinjazz/tested_cell"},
  {:utils, path: "#{__DIR__}/../utils"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.

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

Let's say you're building a family tree but this time, you want to make sure that every Person has a name. You could define a struct with the :name key.

defmodule Person do
  defstruct [:name]
end

You'll notice that structs are defined using modules. The only new concept here is the defstruct construct.

defstruct is an Elixir keyword that means "this module is a struct". You then define a list of keys for the struct that will always exist on the struct.

Using A Struct

A struct is essentially a custom data structure that we've defined. You create instances using %STRUCT{} syntax that looks very similar to how you create a map.

%Person{}

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 name.

%Person{name: "Peter Parker"}

So, why not just use a map? We could easily define a similar map and it looks exactly the same.

%{name: "Peter Parker"}

Unlike maps, Structs provide structure to our data. We can ensure specific keys exist.

For example, using a struct guarantees that certain keys exist, even if the value is nil. So it's always safe to use . syntax. Notice how map.name results in an error, but struct.name simply returns nil.

map = %{}
map.name
struct = %Person{}
struct.name

We also guarantee that other keys don't exist. For example, if we don't define age on our person struct, it can't be set. This helps ensure we use the data structure as intended.

%Person{age: 20}

Multiple Keys

We define the keys for our struct in a list of atoms.

defmodule Character do
  defstruct [:name, :age]
end
%Character{name: "Peter Parker", age: 24}

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.

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

Enter your solution below.

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.

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

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

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

Updating Structs

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

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

Further Reading

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

Commit Your Progress

Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.

$ git add .
$ git commit -m "finish structs section"

Up Next

Previous Next
Modules Rock Paper Scissors Lizard Spock