Skip to content

Latest commit

 

History

History
421 lines (325 loc) · 12.8 KB

supervised_mix_project.livemd

File metadata and controls

421 lines (325 loc) · 12.8 KB

Supervised Mix Projects

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Review Questions

  • What is the benefit of using a supervised mix project?
  • How do you configure a mix project with a supervisor?

Supervised Mix Projects

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

Which creates the following files.

* 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

Starting Our Application

The RockPaperScissors.Application.start/2 function is called when we run the project, and the :mod value in application/0 defines the args argument 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}

Starting Child Workers

We're going to create a RockPaperScissors GenServer in the lib/rock_paper_scissors.ex file which handles the logic for playing our game.

We'll create a very basic named GenServer for now and an IO.puts/2 message to ensure it starts correctly.

defmodule RockPaperScissors do
  use GenServer

  def start_link(opts) do
    IO.puts("RockPaperScissors started")
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  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]}]

RockPaperScissors Game

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.

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.\n" |> 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.\n" |> 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 fault-tolerance in action!

Commit Your Progress

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 Supervised Mix Projects 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.

Navigation