Why do IO lists not make use of the String.Chars protocol?

Hello, I just learned a couple of days ago that IO lists are a thing, and while playing around with them I ran into something that didn’t work when I expected that it would, and I’m curious if it’s something that just isn’t supported at the moment, or if there is some reason it shouldn’t work.

What I expected to work: a struct that implements String.Chars can be used in an IO list directly

Example:

defmodule Test do
  defstruct []
end

defimpl String.Chars, for: Test do
  def to_string(_v), do: "it worked!"
end

Using the contrived example above I expected the following lines would have the same result:

IO.puts "Aaaand...#{%Test{}}"
IO.puts ["Aaaand...", %Test{}]

…but the second line results in an argument error.

I can however make it work if I call to_string explicitly:

IO.puts ["Aaaand...", to_string(%Test{})]
Aaaand...it worked!

I’d love to know why this is the case (as well as if what I tried to do is silly and ruins the performance benefit somehow).

3 Likes

Elixir apparently delegates IO list processing to Erlang and/or the BEAM:

  @spec puts(device, chardata | String.Chars.t) :: :ok
  def puts(device \\ :stdio, item) do
    :io.put_chars map_dev(device), [to_chardata(item), ?\n]
  end

  defp to_chardata(list) when is_list(list), do: list
  defp to_chardata(other), do: to_string(other)

To get the behavior you expect, Elixir would have to call to_string/1 on each item in the list. I lack context for the design decision, but it’s an interesting question.

5 Likes

You already said it… Elixir would need to resolve the protocols before handing over to erlang, this would eat up about every advantage that IO-lists gave us before hand, especially in the case when there are only nested IO-lists.

5 Likes

Ouch ok yeah that makes total sense, and explicitly calling to_string is definitely the better compromise. Thanks for digging into the source like that!

3 Likes