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 can start mix projects under a supervisor.
Use the --sup
flag to generate a supervised mix project.
To demonstrate, we're going to create a supervised rock paper scissors application.
$ mix new rock_paper_scissors --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/rock_paper_scissors.ex
* creating lib/rock_paper_scissors/application.ex
* creating test
* creating test/test_helper.exs
* creating test/rock_paper_scissors_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd rock_paper_scissors
mix test
Run "mix help" for more commands.
This creates a standard mix project, with two main differences.
First, there is a application.ex
file. This file defines a start/2
function which starts a named supervisor RockPaperScissors.Supervisor
.
defmodule RockPaperScissors.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: RockPaperScissors.Worker.start_link(arg)
# {RockPaperScissors.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
end
Next, the mix.exs
file's application/0
function has changed to include a :mod
value.
This :mod
key defines the args value when the RockPaperScissors.Application.start/2
function is called.
def application do
[
extra_applications: [:logger],
mod: {RockPaperScissors.Application, []}
]
end
The RockPaperScissors.Application.start/2
function is called when we run the project, and
the :mod
value in Utils.MixProject
defines second value.
Let's prove that. Add an IO.puts/2 message in the start/2
function. We'll also remove the comments
for the sake of conciseness, but you can leave them in if you prefer.
def start(_type, args) do
IO.puts(args)
children = []
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
We'll also modify args
to prove the connection.
def application do
[
extra_applications: [:logger],
mod: {RockPaperScissors.Application, "Starting Application"}
]
end
Now start the project in the IEx shell and you should see "Starting Application"
in the logs.
$ iex -S mix
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Starting Application
Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
Since the RockPaperScissors.Application.start/2
function starts the RockPaperScissors.Supervisor
it
should be alive. We can use Supervisor.count_children/1 to see that the RockPaperScissors.Supervisor
has
started with no child workers.
iex(1)> Supervisor.count_children(RockPaperScissors.Supervisor)
%{active: 0, specs: 0, supervisors: 0, workers: 0}
We're going to create a RockPaperScissors
worker in the lib/rock_paper_scissors.ex
file which handles the logic for playing our game.
We've seen that a GenServer can be a worker. However, we're not limited to GenServers
.
Let's change things up and use an Agent instead.
We'll create a very basic named Agent for now and an IO.puts/2 message to ensure it starts correctly.
defmodule RockPaperScissors do
use Agent
def start_link(state) do
IO.puts("RockPaperScissors started")
Agent.start_link(fn -> state end, name: RockPaperScissors)
end
def init(state) do
{:ok, state}
end
end
We then have to put the RockPaperScissors
as one of the children
for our supervisor in application.ex
.
def start(_type, args) do
IO.puts(args)
children = [
{RockPaperScissors, []}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
Quit and restart the IEx shell. You should see the Started RockPaperScissors
message in the output.
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Starting Application
RockPaperScissors started
Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
We can confirm the RockPaperScissors
worker is being supervised by the RockPaperScissors.Supervisor
using
Supervisor.which_children/1.
iex(1)> Supervisor.which_children(RockPaperScissors.Supervisor)
[{RockPaperScissors, PID<0.163.0>, :worker, [RockPaperScissors]}]
Now that we have the RockPaperScissors
worker started and supervised, we can create the logic
for the game.
The RockPaperScissors game will prompt the player to enter either rock
, paper
, or scissors
as a string.
$ iex -S mix
iex>
This is going to require message passing, so let's change the RockPaperScissors
module into a GenServer.
defmodule RockPaperScissors do
use GenServer
def start_link(state) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
{:ok, state}
end
end
We want to repeatedly prompt the user to enter a choice for rock, paper, or scissors.
So we'll send the RockPaperScissors
module a :prompt
message over and over again.
defmodule RockPaperScissors do
use GenServer
def start_link(state) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
send(self(), :prompt)
{:ok, state}
end
def handle_info(:prompt, state) do
choice = "Please enter rock, paper, or scissors." |> IO.gets() |> String.trim()
send(self(), :prompt)
{:noreply, state}
end
end
Now, you'll notice that if we try to start the project in the IEx shell, we can't actually interact with the console to provide the player's choice.
$ iex -S mix
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
We can instead run a mix project with mix run
. However, mix run
will exit once the RockPaperScissors.start/2
callback
completes.
$ mix run
mix run
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
$
We can use the --no-halt
flag to avoid this issue.
Now we can keep entering a choice over and over.
$ mix run --no-halt
mix run
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
rock
Please enter rock, paper, or scissors.
paper
Please enter rock, paper, or scissors.
Now we've got everything in place to implement the actual rock paper scissors game.
defmodule RockPaperScissors do
use GenServer
def start_link(state) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
send(self(), :prompt)
{:ok, state}
end
def handle_info(:prompt, state) do
choice = "Please enter rock, paper, or scissors." |> IO.gets() |> String.trim()
answer = Enum.random(["rock", "paper", "scissors"])
result =
case {choice, answer} do
{"rock", "scissors"} -> "You win!"
{"paper", "rock"} -> "You win!"
{"scissors", "paper"} -> "You win!"
{"rock", "paper"} -> "You Lose!"
{"paper", "scissors"} -> "You Lose!"
{"scissors", "rock"} -> "You Lose!"
{same, same} -> "Draw!"
end
IO.puts(result)
send(self(), :prompt)
{:noreply, state}
end
end
Now we can play the game!
$ mix run --no-halt
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
rock
You Win!
Please enter rock, paper, or scissors.
However, to demonstrate the benefit of our supervisor, we've purposely left a bug in the code above. The case
statement doesn't handle input
other than "rock"
, "paper"
, or "scissors"
. Even "Rock"
, "Paper"
, and "Scissors"
are not valid answers.
$ mix run --no-halt
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
Rock
19:34:09.995 [error] GenServer RockPaperScissors terminating
** (CaseClauseError) no case clause matching: {"Rock", "scissors"}
(rock_paper_scissors 0.1.0) lib/rock_paper_scissors.ex:20: RockPaperScissors.handle_info/2
(stdlib 3.17.1) gen_server.erl:695: :gen_server.try_dispatch/4
(stdlib 3.17.1) gen_server.erl:771: :gen_server.handle_msg/6
(stdlib 3.17.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: :prompt
State: []
Please enter rock, paper, or scissors.
Notice that even though our game crashes, the supervisor restarts the RockPaperScissors
process
so we can keep playing. That's the definition of fault-tolerance!
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 supervised mix project section"
Previous | Next |
---|---|
Supervisors | Supervised Stack |