1   Description

This blog post provides yet another example of the definition and use of Elixir protocols. You can find others by, for example, searching the Web for "elixir protocol".

I was intriged in this sentence in the help for module Protocol (in IEx, do h Protocol):

The real benefit of protocols comes when mixed with structs.

Part of that benefit comes from the ability to dispatch off of different Struct definitions. This is analogous to our ability to use pattern matching to determine whether a data item is an instance of a particular Struct definition. For example, with the pattern matching operator =/2, we can do:

iex> b1 = %BirdData{name: "magpie", sightingdate: "02/03/2020"}
%BirdData{name: "magpie", sightingdate: "02/03/2020"}
iex> %BirdData{} = b1
%BirdData{name: "magpie", sightingdate: "02/03/2020"}
iex> %TreeData{} = b1
** (MatchError) no match of right hand side value: %BirdData{name: "magpie", sightingdate: "02/03/2020"}

Or, in a case statement, we can do:

def test() do
  bird1 = %BirdData{name: "blackheaded phoebe", sightingdate: "03/01/2020"}
  tree1 = %TreeData{name: "valley oak", avgheight: "50 ft.", leaftype: "flat"}
  #MultiStructAPI.show(bird1)
  [bird1, tree1, "something different"]
  |> Enum.each(fn item ->
    case item do
      %BirdData{} -> IO.puts("It's a bird")
      %TreeData{} -> IO.puts("It's a tree")
      _ -> IO.puts("It's weird")
    end
  end)
end

So, we can think of Elixir protocols when used with Elixir Structs to be convenience wrappers around case statements (analogous to the one above) that dispatch on the Struct type of the first argument passed to a function.

2   Some sample code

Here is the sample code. Notes follow:

defprotocol MultiStructAPI do
  def show(data)
  def concatenate(data)
end

defmodule BirdData do
  defstruct [name: "", sightingdate: "00/00/0000"]
  defimpl MultiStructAPI do
    def show(data) do
      IO.puts("Bird -- name: #{data.name}  date: #{data.sightingdate}")
    end
    def concatenate(data) do
      "#{data.name}::#{data.sightingdate}"
    end
  end
end

defmodule TreeData do
  defstruct [name: "", avgheight: nil, leaftype: "<unknown>"]
  defimpl MultiStructAPI do
    def show(data) do
      IO.puts("Tree -- name: #{data.name}  average height: #{data.avgheight}  leaf: #{data.leaftype}")
    end
    def concatenate(data) do
      "#{data.name}::#{data.avgheight}::#{data.leaftype}"
    end
  end
end

defmodule MultiStructAPITest do
  def test1() do
    bird1 = %BirdData{name: "blackheaded phoebe", sightingdate: "03/01/2020"}
    tree1 = %TreeData{name: "valley oak", avgheight: "50 ft.", leaftype: "flat"}
    #MultiStructAPI.show(bird1)
    items = [bird1, tree1]
    items |> Enum.each(fn item ->
      MultiStructAPI.show(item)
    end)
  end
  def test2() do
    bird1 = %BirdData{name: "blackheaded phoebe", sightingdate: "03/01/2020"}
    tree1 = %TreeData{name: "valley oak", avgheight: "50 ft.", leaftype: "flat"}
    #MultiStructAPI.show(bird1)
    [bird1, tree1]
    |> Enum.each(fn item ->
      IO.puts(MultiStructAPI.concatenate(item))
    end)
  end
end

Notes:

  • We use the defprotocol macro to define our API: MultiStructAPI. Our API contains the functions show/1 and concatenate/1.
  • Notice that the first argument in each of these functions is the value whose type (structure definition) we will dispatch on.
  • For each structure or data type which we want to use this API, we define an implementation module. In our example above, these are BirdData, TreeData.
  • In that implementation module (1) we define the struct (using macro defstruct) and (2) we implement the functions in our API (inside the defimpl macro).

Published

Category

elixir

Tags

Contact