Subscribe now

Preparing to Learn About Ecto [03.29.2017]

Ecto refers to itself as a domain specific language for writing queries and interacting with databases in Elixir. It's the layer via which Phoenix applications interact with a database by default, but of course it's usable outside of Phoenix as well. We'll learn about it this week.

First, let's look at the solution to last week's exercise.

Exercise Solution

I'm starting with the dailydrip/rpn repo tagged before this episode.

First, we'll switch to using a GenServer:

defmodule Rpn do
  use GenServer

  ### Client API

  def start do
    GenServer.start(__MODULE__, [])
  end


  def peek(pid) do
    GenServer.call(pid, :peek)
  end

  def push(pid, op) do
    GenServer.cast(pid, {:push, op})
  end

  ### Server API

  def init() do
    {:ok, []}
  end

  def handle_call(:peek, _from, state) do
    {:reply, state, state}
  end

  def handle_cast({:push, :+}, [ second | [ first | rest ] ]) do
    {:noreply, [(first + second) | rest]}
  end
  def handle_cast({:push, :-}, [ second | [ first | rest ] ]) do
    {:noreply, [(first - second) | rest]}
  end
  def handle_cast({:push, :x}, [ second | [ first | rest ] ]) do
    {:noreply, [(first * second) | rest]}
  end
  def handle_cast({:push, val}, state) do
    {:noreply, [val | state]}
  end
end

That works fine. Let's switch from start to start_link on the path to supervision next. First we update the test:

defmodule RpnTest do
  use ExUnit.Case

  test "starts with an empty stack" do
    {:ok, pid} = Rpn.start_link
    # ...
  end

  test "pushing onto the stack" do
    {:ok, pid} = Rpn.start_link
    # ...
  end

  test "adding" do
    {:ok, pid} = Rpn.start_link
    # ...
  end

  test "subtracting" do
    {:ok, pid} = Rpn.start_link
    # ...
  end

  test "multiplying" do
    {:ok, pid} = Rpn.start_link
    # ...
  end

  test "wikipedia example" do
    {:ok, pid} = Rpn.start_link
    # ...
  end
end

Then we update the module:

defmodule Rpn do
  use GenServer

  ### Client API

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end
  # ...
end

The tests should still pass. Next, we can supervise it. I don't expect this to have been easy, or maybe even achievable, without a great deal of reading. That's because when we started this project, we started it as a library application rather than as a supervised application. That means we get to learn how these two things differ in the Mixfile. You could create a couple of applications and compare them to figure this out for yourself, if you'd like, with:

cd ~/tmp
mkdir mixtmp
cd mixtmp
mix new foo
mix new bar --sup

But that's not so important as I'll walk you through everything that's different. First, our mix.exs file wants a mod key in the Keyword List that's returned from the application function:

defmodule Rpn.Mixfile do
  # ...
  def application do
    [
      extra_applications: [:logger],
      mod: {Rpn.Application, []} # <--
    ]
  end
  # ...
end

If you want to see what this does, and more, you can run:

mix help compile.app

This will tell you all about .app files. The bit we care about is the mod key:

  • :mod - specifies a module to invoke when the application is started. It
    must be in the format {Mod, args} where args is often an empty list. The
    module specified must implement the callbacks defined by the Application
    module.

So we need to specify an Application module here. Let's create one:

mkdir lib/rpn
vim lib/rpn/application.ex
defmodule Rpn.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
    ]

    opts = [strategy: :one_for_one, name: Rpn.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

This starts no children. We'd like to start the calculator and supervise it. This means we want to be able to tell it what its name is. We've done this before. First we'll add the worker to our supervisor, then we'll update the Rpn module to take an argument that provides options:

defmodule Rpn.Application do
  # ...
  def start(_type, _args) do
    # ...
    children = [
      worker(Rpn, [[name: Rpn]])
    ]
    # ...
  end
end
defmodule Rpn do
  # ...
  def start_link(options \\ []) do
    GenServer.start_link(__MODULE__, [], options)
  end
  # ...
end

Now our calculator should be up and operating when we start our application. We can try this with iex -S mix:

iex(1)> Rpn.push(Rpn, 1)
:ok

You can verify it gets restarted when it crashes yourself.

Finally, we'll add a TapePrinter GenServer that gets started earlier in the supervisor, and message to it from our calculator. The TapePrinter is very simple. We'll build it without tests because I'm a terrible person.

defmodule Rpn.TapePrinter do
  use GenServer
  @name TapePrinter

  ### Client API

  def start_link do
    GenServer.start_link(__MODULE__, [], [name: @name])
  end

  def print(term) do
    GenServer.cast(@name, {:print, term})
  end

  ### Server API

  def init do
    {:ok, []}
  end

  def handle_cast({:print, term}, state) do
    IO.puts term
    {:noreply, [term | state]}
  end
end

We're accumulating each printed term and then outputting it to the console, and we've hardcoded the name. Let's start it from the supervisor as well, before the calculator:

defmodule Rpn.Application do
  # ...
  def start(_type, _args) do
    # ...
    children = [
      worker(Rpn.TapePrinter, []),
      worker(Rpn, [[name: Rpn]])
    ]
    # ...
  end
end

And now we'll send it messages any time we evaluate an operation, from the Rpn GenServer:

defmodule Rpn do
  # ...
  alias Rpn.TapePrinter
  # ...
  def handle_cast({:push, :+}, [ second | [ first | rest ] ]) do
    val = first + second
    TapePrinter.print(val)
    {:noreply, [val | rest]}
  end
  def handle_cast({:push, :-}, [ second | [ first | rest ] ]) do
    val = first - second
    TapePrinter.print(val)
    {:noreply, [val | rest]}
  end
  def handle_cast({:push, :x}, [ second | [ first | rest ] ]) do
    val = first * second
    TapePrinter.print(val)
    {:noreply, [val | rest]}
  end
  # ...
end

Now if you use the calculator, you'll see that it prints to the tape:

iex(1)> Rpn.push(Rpn, 1)
:ok
iex(2)> Rpn.push(Rpn, 2)
:ok
iex(3)> Rpn.push(Rpn, :+)
:ok
3

Kudos if you did the extra work. Feel free to share any questions you have in the comments section and I'll try to be helpful, or join us on our Slack channel for quicker replies.

Preparatory Readings

Now let's prepare to learn about Ecto. This week we'll be interacting with a database, so make sure you have PostgreSQL installed. Here are a few more resources, ranging from introductory to pretty detailed. Read as much or as little as you'd like, and we'll start modeling the Firestorm Forum's data layer in Ecto this week.

See you soon!

Resources