Enum.into/2 with String as Collectable vs. List.to_string/1

I am trying to understand the protocol aspect of Elixir better, and stumbled upon something I do not quite get about the interplay between Enumerable and Collectable. The Enum.into/2 function utilizes the Collectable protocol for its second argument. Since a string is reported as implementing the Collectable protocol by IEx, I would expect the following to be an identity:

"hello"
|> String.to_charlist()
|> Enum.into("")

However, this does not work as I expect (using Elixir 1.12). Instead I get a rather cryptic error message, at least to me:

** (FunctionClauseError) no function clause matching in anonymous fn/2 in Collectable.BitString.into/1

    The following arguments were given to anonymous fn/2 in Collectable.BitString.into/1:

        # 1
        [""]

        # 2
        {:cont, 104}

    (elixir 1.12.0) anonymous fn/2 in Collectable.BitString.into/1
    (elixir 1.12.0) lib/enum.ex:2356: Enum."-into/4-lists^foldl/2-0-"/3
    (elixir 1.12.0) lib/enum.ex:2356: Enum.into/4

If I instead use the following pipeline, I get the identity as expected:

"hello"
|> String.to_charlist()
|> List.to_string()

What am I missing? I get that the latter works, but would still like to understand the former.

Thanks in advance!

You want

"hello" |> String.codepoints() |> Enum.into("")

because Sring.to_charlist is not doing what you expect. It’s converting to a list of integers and the wrong Collectable implementation is chosen.

5 Likes

It’s not that String doesn’t work as Collectable; it’s that Char lists are lists of Integers:

String.to_charlist("hello") == [104, 101, 108, 108, 111]

and you’re trying to insert them into a String. For your case it would be better to use

"hello"
|> String.graphemes
|> Enum.into("")

as

String.graphemes("hello") == ["h", "e", "l", "l", "o"]
5 Likes