Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively, you can evaluate the Elixir cells as you read.
Behaviours allow us to create modules that all share a common pattern but need different implementations.
You can define a behaviour, and then two other modules will implement it.
flowchart
A[Behaviour] --> B[Module]
A[Behaviour] --> C[Module]
This enforces that both modules will implement a shared application programming interface or API. What is an API? Well, in this specific context the API is the set of public-facing functions in a module. More generically an interface is the point at which two systems interact. Since we're talking about systems for programming applications, we call it the application programming interface.
To define a behaviour, we start by defining a module. However, instead of implementing
functions, we create @callback
module attributes. These @callback module attributes
define the signature of a function.
The signature of a function is the name, params, and expected return value, but not the implementation. Essentially, it's the expected input, and output types in our program, but not the actual implementation.
To define the expected return value we use ::
and then the data type we expect to return.
Elixir provides a set of types. You can see the full list of types in the Typespecs
documentation. For now, we're going to use the String.t()
type. Some other common
types are atom()
, boolean()
, integer()
, float()
, and any()
if you don't require a specific output.
defmodule Villain do
@callback catchphrase() :: String.t()
end
Now a module can use the Villain
behaviour using the @behaviour
module attribute.
You'll notice that Elixir displays a warning because the DarthVader
module is supposed
to implement a catchphrase/0
function.
defmodule DarthVader do
@behaviour Villain
end
To remove the warning, we need to implement my_function/0
and it should return a string.
defmodule DarthVader do
@behaviour Villain
def catchphrase do
"No, I am your father."
end
end
DarthVader.catchphrase()
So, why use a behaviour?
Primarily, behaviours ensure that any module that implements the behaviour provides the expected set of functions and that those functions match the expected function signatures.
Behaviours are another way to achieve polymorphism
. We can define
a behaviour, and then several different modules can implement that behaviour. Behaviours
allow us to create "templates" for modules to ensure each module is consistent.
The caller, which is the place in the code where we call a module's function, can then have a consistent experience between modules.
flowchart
A --> C
A --> D
A --> E
A --> F
C --> B
D --> B
E --> B
F --> B
A[Caller]
B[Behaviour]
C[Implementation]
D[Implementation]
E[Implementation]
F[Implementation]
If the behaviour ever needs to change, we can modify the behaviour, and know which modules need to be updated thanks to the warnings Elixir provides.
You can also implement behaviours with dynamic dispatching which allows the caller to deal with a single module, and swap out the implementation.
flowchart
A --> B
B --> C
B --> D
B --> E
B --> F
A[Caller]
B[Behaviour]
C[Implementation]
D[Implementation]
E[Implementation]
F[Implementation]
But that's beyond the scope of this lesson.
In the Elixir cell below, convert the Pet
module into a behavior.
The Pet
behavior should define an @callback
for a speak/0
function that returns a string.
defmodule Pet do
end
Once complete, re-evaluate the following module. It should display a warning.
speak/0 required by behaviour Pet is not implemented (in module Dog)
Add a speak/0
function to the Dog
module. When you re-evaluate the cell, the warning should be gone.
defmodule Dog do
@behaviour Pet
end
file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")
progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()
default = Map.get(existing_progress, file_name, false)
form =
Kino.Control.form(
[
completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
],
report_changes: true
)
Task.async(fn ->
for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
File.write!(progress_path, Jason.encode!(Map.put(existing_progress, file_name, completed)))
end
end)
form
Run the following in your command line from the curriculum folder to track and save your progress in a Git commit.
Ensure that you do not already have undesired or unrelated changes by running git status
or by checking the source control tab in Visual Studio Code.
$ git checkout solutions
$ git checkout -b behaviours-reading
$ git add .
$ git commit -m "finish behaviours reading"
$ git push origin behaviours-reading
Create a pull request from your behaviours-reading
branch to your solutions
branch.
Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.
DockYard Academy Students Only:
Notify your instructor by including @BrooklinJazz
in your PR description to get feedback.
You (or your instructor) may merge your PR into your solutions branch after review.
If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.