Mix.install([
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:tested_cell, github: "brooklinjazz/tested_cell"},
{:utils, path: "#{__DIR__}/../utils"}
])
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
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.
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.
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}
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}
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
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
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.
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)
- 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 thehero
struct instance and return a greeting.
Hero.greeting(hero) # "I am Spider-Man."
- Create a
Hero.reveal/1
function which accepts thehero
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.
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"}
Consider the following resource(s) to deepen your understanding of the topic.
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"
Previous | Next |
---|---|
Modules | Rock Paper Scissors Lizard Spock |