How does protocol dispatch work?

For example, how does Kernel‘s to_string “know” that it belongs to String.Chars?

If I do a defimpl String.Chars, for: MyMod – how does to_string know
to call my implemented function?

Just curious.

The module String.Chars has a function of to_string, and the kernel to_string just calls String.Chars.to_string.

What a protocol does it auto-generate the protocol functions from a list of definitions. So when you defimpl String.Chars, for: MyMod or whatever the syntax was then it defines a module, and at protocol consolidation time it scans all the compiled modules to see what is impl’d (just an attribute marker is all it is, of protocol_impl, so for your example it would have an attribute on the module of protocol_impl: [protocol: String.Char, for: MyMod]), and it uses those to build functions that just test, so in your example it would generate a function of:

defmodule String.Chars do
  # ... other stuff
  def to_string(%MyMod{}=v), do: String.Chars.MyMod.to_string(v)
  # ... other stuff
end

I have an enhanced Protocol library that you can see the code of how it works if curious too. Can also look at the elixir code as well. :slight_smile:

2 Likes

This does only explain it roughly, but should be just enough to understand it.

  1. Kernel.to_string/1 is a macro, expanding to String.Chars.to_string/1
  2. String.Chars.to_string/1 matches on its argument, and without protocol consolidation it dispatches to String.Chars.xxx.to_string/1 where xxx is the qualified alias of the struct or elixirs internal type. With consolidation those xxx implementations are integrated directly into String.Chars.to_string/1.

With consolidation it might look like this (simplified):

defmodule String.Chars do
  def to_string(list) when is_list(list), do: List.to_string(list)
  def to_string(%Foo{bar: bar}), do: "#Foo<bar: #{bar}>"
end

without consilidation more like this:

defmodule String.Chars do
  def to_string(list) when is_list(list), do: List.to_string(list)
  def to_string(%struct{} = item), do: :"Elixir.#{__MODULE__}.#{struct}" |> apply(:to_string, [item])
end
2 Likes