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).