Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
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?
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.
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.
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. Enforce these 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, 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 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]
Consider the following resource(s) to deepen your understanding of the topic.
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.