Manuel Albarran

A random notes about Elixir

Behaviours vs. Protocols

14 Feb 2017 » elixir, polymorphism, behaviour, protocol

source / via

A protocol is indeed a behaviour + dispatching logic.

However I think you are missing the point of behaviours. Behaviours are extremely useful. For example, a GenServer defines a behaviour. A behaviour is a way to say: give me a module as argument and I will invoke the following callbacks on it, which these argument and so on. A more complex example for behaviours besides a GenServer are the Ecto adapters.

However, this does not work if you have a data structure and you want to dispatch based on the data structure. Hence protocols.

Elixir’s Behaviours vs Protocols

Behaviours answer the questions:

  • “How can I define a public contract/spec for my modules to implement?”
  • “How can I write code that can be extended by others?”
  • “How would I go about writing a plugin/adapter based system?”
  • “How can I write calling code to work with many swappable modules?”

Protocols answer the questions:

  • “How can I have different implementations of the same function based on the different types I’m working with?”
  • “How can I write code that can be extended to work with new data types I don’t know about yet?”

Why is Access a behaviour instead of a Protocol?…

Access was a protocol initially, but I think it was changed to a behaviour because of performance issues around protocol dispatch. It was simply too slow for something used as frequently as access.

  • michalmuskala

Writing extensible Elixir with Behaviours

defmodule Swoosh.Adapter do
  @moduledoc ~S"""
  Specification of the email delivery adapter.
  """

  @type t :: module

  @type email :: Swoosh.Email.t

  @typep config :: Keyword.t

  @doc """
  Delivers an email with the given config.
  """
  @callback deliver(email, config) :: {:ok, term} | {:error, term}
end
defmodule Swoosh.Adapters.Local do

  @behaviour Swoosh.Adapter

  def deliver(%Swoosh.Email{} = email, _config) do
    %Swoosh.Email{headers: %{"Message-ID" => id}} = Swoosh.InMemoryMailbox.push(email)
    {:ok, %{id: id}}
  end
end

BASIC

defmodule Circle0, do: defstruct [:r]
defmodule Square0, do: defstruct [:a]
defmodule Rectangle0, do: defstruct [:w, :h]

defmodule Polygon do
  @doc "Calculates the area of a surface"
  def area(%Circle0{r: r}), do: :math.pi * r * r
  def area(%Square0{a: a}), do: a * a
  def area(%Rectangle0{w: w, h: h}), do: w * h

  @doc "Calculates the perimeter of a surface"
  def perimeter(data)
  def perimeter(%Circle0{r: r}), do: 2 * :math.pi * r
  def perimeter(%Square0{a: a}), do: 4 * a
  def perimeter(%Rectangle0{w: w, h: h}), do: 2 * w + 2 * h

  def roudness(polygon) do
    area = polygon |> area
    circle_area = polygon |> perimeter |> circle_area_from_perimeter
    area / circle_area
  end

  defp circle_area_from_perimeter(perimeter) do
    perimeter * perimeter / (4 * :math.pi)
  end
end

%Circle0{r: 10} |> Polygon.area
#> 304.0592653589793
%Circle0{r: 10} |> Polygon.perimeter
#> 62.83085307079586
%Circle0{r: 10} |> Polygon.roudness
#> 0.0

%Square0{a: 10} |> Polygon.area
#> 100
%Square0{a: 10} |> Polygon.perimeter
#> 40
%Square0{a: 10} |> Polygon.roudness
#> 0.7853980633974483

%Rectangle0{w: 10, h: 5} |> Polygon.area
#> 50
%Rectangle0{w: 10, h: 5} |> Polygon.perimeter
#> 30
%Rectangle0{w: 10, h: 5} |> Polygon.roudness
#> 0.6981317007977318

PROTOCOL

defprotocol Geometry do
  @doc "Calculates the area of a surface"
  def area(data)
  @doc "Calculates the perimeter of a surface"
  def perimeter(data)
end

defmodule Roudness do
  def roudness(geometry) do
    area = geometry |> Geometry.area
    circle_area = geometry |> Geometry.perimeter |> circle_area_from_perimeter
    area / circle_area
  end

  defp circle_area_from_perimeter(perimeter) do
    perimeter * perimeter / (4 * :math.pi)
  end
end

defmodule Circle1 do
  defstruct [:r]
end

defimpl Geometry, for: Circle1 do
  def area(%Circle1{r: r}), do: :math.pi * r * r
  def perimeter(%Circle1{r: r}), do: 2 * :math.pi * r
end

defmodule Square1 do
  defstruct [:a]
end

defimpl Geometry, for: Square1 do
  def area(%Square1{a: a}), do: a * a
  def perimeter(%Square1{a: a}), do: 4 * a
end

defmodule Rectangle1 do
  defstruct [:w, :h]
end

defimpl Geometry, for: Rectangle1 do
  def area(%Rectangle1{w: w, h: h}), do: w * h
  def perimeter(%Rectangle1{w: w, h: h}), do: 2 * w + 2 * h
end

%Circle1{r: 10} |> Geometry.area
#> 314.1592653589793
%Circle1{r: 10} |> Geometry.perimeter
#> 62.83185307179586
%Circle1{r: 10} |> Roudness.roudness
#> 1.0

%Square1{a: 10} |> Geometry.area
#> 100
%Square1{a: 10} |> Geometry.perimeter
#> 40
%Square1{a: 10} |> Roudness.roudness
#> 0.7853981633974483

%Rectangle1{w: 10, h: 5} |> Geometry.area
#> 50
%Rectangle1{w: 10, h: 5} |> Geometry.perimeter
#> 30
%Rectangle1{w: 10, h: 5} |> Roudness.roudness
#> 0.6981317007977318

BEHAVIOUR

defmodule Surface do
  @moduledoc "Specification of a surface."
  @type t :: module

  @doc "Calculates the area of a surface"
  @callback area(t) :: number
  @doc "Calculates the perimeter of a surface"
  @callback perimeter(t) :: number
end

defmodule SurfaceRoudness do
  defmacro __using__(_) do
    quote do
      def roudness(surface) do
        area = surface |> area
        circle_area = surface |> perimeter |> circle_area_from_perimeter
        area / circle_area
      end

      defp circle_area_from_perimeter(perimeter) do
        perimeter * perimeter / (4 * :math.pi)
      end
    end
  end
end

defmodule Circle2 do
  defstruct [:r]

  @behaviour Surface
  use SurfaceRoudness

  def area(%Circle2{r: r}), do: :math.pi * r * r
  def perimeter(%Circle2{r: r}), do: 2 * :math.pi * r
end

defmodule Square2 do
  defstruct [:a]

  @behaviour Surface
  use SurfaceRoudness

  def area(%Square2{a: a}), do: a * a
  def perimeter(%Square2{a: a}), do: 4 * a
end

defmodule Rectangle2 do
  defstruct [:w, :h]

  @behaviour Surface
  use SurfaceRoudness

  def area(%Rectangle2{w: w, h: h}), do: w * h
  def perimeter(%Rectangle2{w: w, h: h}), do: 2 * w + 2 * h
end


%Circle2{r: 10} |> Circle2.area
#> 314.1592653589793
%Circle2{r: 10} |> Circle2.perimeter
#> 62.83185307179586
%Circle2{r: 10} |> Circle2.roudness
#> 1.0

%Square2{a: 10} |> Square2.area
#> 100
%Square2{a: 10} |> Square2.perimeter
#>  40
%Square2{a: 10} |> Square2.roudness
#> 0.7853981633974483

%Rectangle2{w: 10, h: 5} |> Rectangle2.area
#> 50
%Rectangle2{w: 10, h: 5} |> Rectangle2.perimeter
#> 30
%Rectangle2{w: 10, h: 5} |> Rectangle2.roudness
#> 0.6981317007977318

BEHAVIOUR via macro

defmodule Surfer do
  @moduledoc "Specification of a surface."
  @type t :: module

  @doc "Calculates the area of a surface"
  @callback area(t) :: number
  @doc "Calculates the perimeter of a surface"
  @callback perimeter(t) :: number

  defmacro __using__(_) do
    quote do
      @behaviour Surfer

      def roudness(surfer) do
        area = surfer |> area
        circle_area = surfer |> perimeter |> circle_area_from_perimeter
        area / circle_area
      end

      defp circle_area_from_perimeter(perimeter) do
        perimeter * perimeter / (4 * :math.pi)
      end
    end
  end
end

defmodule Circle3 do
  defstruct [:r]

  use Surfer

  def area(%Circle3{r: r}), do: :math.pi * r * r
  def perimeter(%Circle3{r: r}), do: 2 * :math.pi * r
end

defmodule Square3 do
  defstruct [:a]

  use Surfer

  def area(%Square3{a: a}), do: a * a
  def perimeter(%Square3{a: a}), do: 4 * a
end

defmodule Rectangle3 do
  @doc ~S"""

  ## Examples
      iex> %Rectangle3{w: 10, h: 5} |> Rectangle3.area
      50
      iex> %Rectangle3{w: 10, h: 5} |> Rectangle3.perimeter
      30
      iex> %Rectangle3{w: 10, h: 5} |> Rectangle3.roudness
      0.6981317007977318
  """
  defstruct [:w, :h]

  use Surfer

  def area(%Rectangle3{w: w, h: h}), do: w * h
  def perimeter(%Rectangle3{w: w, h: h}), do: 2 * w + 2 * h
end


ExUnit.start()
defmodule Rectangle3.Test do
  use ExUnit.Case, async: true
  doctest Rectangle3
end



%Circle3{r: 10} |> Circle3.area
#> 314.1592653589793
%Circle3{r: 10} |> Circle3.perimeter
#> 62.83185307179586
%Circle3{r: 10} |> Circle3.roudness
#> 1.0

%Square3{a: 10} |> Square3.area
#> 100
%Square3{a: 10} |> Square3.perimeter
#>  40
%Square3{a: 10} |> Square3.roudness
#> 0.7853981633974483

%Rectangle3{w: 10, h: 5} |> Rectangle3.area
#> 50
%Rectangle3{w: 10, h: 5} |> Rectangle3.perimeter
#> 30
%Rectangle3{w: 10, h: 5} |> Rectangle3.roudness
#> 0.6981317007977318