While we tend to associate polymorphism with object-oriented design, the functional language, Elixir, allows us to create duck types through a mechanism called protocols.

Let’s imagine we have an Elixir game program that has a board of tiles. A tile can either be a BombTile struct or an EmptyTile struct.

defmodule BombTile do
  defstruct exploded: false
end

defmodule EmptyTile do
  defstruct revealed: false
end

Each of these structs has a different property: :exploded and :revealed, which are false by default. When a user clicks on a tile, we expect it to either explode if it’s a bomb, or be revealed if it’s empty.

In order to change the :exploded and :revealed properties on the tiles to true , we’ll add a select/1 function to each module.

defmodule BombTile do
  defstruct exploded: false

  def select(bomb_tile) do
    %{ bomb_tile | exploded: true }
  end
end

defmodule EmptyTile do
  defstruct revealed: false

  def select(empty_tile) do
    %{ empty_tile | revealed: true }
  end
end

Now, let’s add a Board module that contains a function to “select” a tile at a given index on a board (because everything is immutable in Elixir, this function is going to return a new list of tiles).

defmodule Board do
  def select_tile(board, index) do
    tile = Enum.at(board, index)

    selected_tile =
      cond do
        tile.__struct__ == BombTile ->
          BombTile.select(tile)

        tile.__struct__ == EmptyTile ->
          EmptyTile.select(tile)
      end

    List.replace_at(board, index, selected_tile)
  end
end

Because we haven’t implemented polymorphism yet, we have to check the struct of the tile in order to know which module to send the select/1 message to. Now let’s see this code in action.

iex> board = [%BombTile{}, %EmptyTile{}]
[
  %BombTile{exploded: false},
  %EmptyTile{revealed: false}
]

iex> Board.select_tile(board, 0)
[
  %BombTile{exploded: true},
  %EmptyTile{revealed: false}
]

iex> Board.select_tile(board, 1)
[
  %BombTile{exploded: false},
  %EmptyTile{revealed: true}
]

It works as expected, but this antipattern is reminiscent of the one we discovered in our discussion of duck typing in object-oriented design. Even in a functional language, checking the type of a data structure before deciding which message to send is a code smell. Let’s fix this by implementing a Tile protocol.

defprotocol Tile do
  def select(tile)
end

This is the protocol definition. Any module that implements a protocol needs to implement the functions defined in said protocol. Now let’s implement the Tile protocol for our BombTile and EmptyTile modules.

defimpl Tile, for: BombTile do
  def select(bomb_tile) do
    %{ bomb_tile | exploded: true }
  end
end

defimpl Tile, for: EmptyTile do
  def select(empty_tile) do
    %{ empty_tile | revealed: true }
  end
end

The first argument of each function has to be a data type that implements the protocol. Also, remember to remove the original select/1 functions from BombTile and EmptyTile as they’re no longer needed there.

Now, let’s refactor our Board.select/2 function to use the Tile protocol.

defmodule Board do
  def select_tile(board, index) do
    List.replace_at(board, index, Tile.select(tile))
  end
end

And now you know how to achieve polymorphism in Elixir!