Protocols enable polymorphism in Elixir. Define protocols with defprotocol:

defprotocol Log do
  def log(value, opts)

Implement a protocol with defimpl:

require Logger
# User and Post are custom structs

defimpl Log, for: User do
  def log(user, _opts) do "User: #{}, #{user.age}"

defimpl Log, for: Post do
  def log(user, _opts) do "Post: #{post.title}, #{post.category}"

Instead of sharing protocol implementation with maps, structs require their own protocol implementation.

With the above implementations, we can do:

iex> Log.log(%User{name: "Yos", age: 23})
22:53:11.604 [info]  User: Yos, 23
iex> Log.log(%Post{title: "Protocols", category: "Protocols"})
22:53:43.604 [info]  Post: Protocols, Protocols

Protocols let you dispatch to any data type, so long as it implements the protocol. This includes some built-in types such as Atom, BitString, Tuples, and others.

Other Examples

Poison allows developers to implement Encoders for their own structs for serializing them to JSON, by implementing the Poison.Encoder protocol.

defimpl Poison.Encoder, for: Person do
  def encode(%{name: name, age: age}, options) do
    Poison.Encoder.BitString.encode("#{name} (#{age})", options)

Additional reading: