String.to_existing_atom

In a mix project, I have the file lib/stuff.ex:

defmodule User do
  defstruct [:name, :age]
end

defmodule Stuff do
  IO.puts "Stuff"
end

defmodule Dog do
  IO.puts "Dog"
end

defmodule Cat do
  IO.puts "Cat"
end

I have another file lib/app1.ex:

  def go do
     
    [["Cat"], ["Dog"]]
    |> Enum.map(fn name -> 
              IO.inspect name
              str = "Elixir.#{name}"
              IO.inspect str
              String.to_existing_atom(str) 
            end)
  end

Here’s the output:

~/elixir_programs/app1$ iex -S mix
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Compiling 1 file (.ex)
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> App1.go

["Cat"]

"Elixir.Cat"

["Dog"]

"Elixir.Dog"

[Cat, Dog]

Can anyone explain how String.to_existing_atom("Elixir.[Cat]") succeeds?

When you have

defmodule Cat do
  IO.puts "Cat"
end

The full name of the module is the atom :Elixir.Cat. Thus when you set your str variable to "Elixir.Cat" and then call String.to_existing_atom on it, it comes back with the atom :Elixir.Cat. This article goes into more detail about the whole process:

https://www.honeybadger.io/blog/elixir-module-names/

4 Likes

Your code isn’t actually executing String.to_existing_atom("Elixir.[Cat]"). The str = "Elixir.#{name}" will return "Elixir.Cat", not "Elixir.[Cat]". The reason is that string interpolation returns a string (no surprise). So "Elixir.#{name}" when name is ["Cat"] will return "Elixir.Cat". You can try it by:

iex> c = ["cat"]
["cat"]
iex> "#{c}"
"cat"
iex> c = [["cat"], ["dog"]]
[["cat"], ["dog"]]
iex> "#{c}"                
"catdog"
4 Likes

I did try that, but somehow I thought I got "Elixir.[Cat]".

iex> c = [["cat"], ["dog"]
"catdog"

Whaaaa?

The reason is that string interpolation returns a string (no surprise)

lol. I don’t think that is a reason at all. I think everyone has seen the error:

String.chars not implement for “THE BlASTED THING I WANT TO PRINT OUT”

Why would String.chars be implemented for the list:

[["cat"], ["dog"]]

but not the map:

 map = %{"a": 1, b: "hello"}

After seeing your example, I have to admit I thought the map might interpolate as:

 "a": "hello"

String interpolation of "This plus #{var} and this" expands during compilation to "This plus " <> Kernel.to_string(var) <> " and this". The String.Chars protocol is implemented for lists (calling recursively over the elements of the list) as you see. It can still fail, of course, if one of the list elements doesn’t implement String.Chars.

Its more useful than you might think given that the standard Erlang string type is a charlist. For example:

iex> to_string 'abcdef'
"abcdef"
iex> to_string [?a, ?b, ?c, ?d, ?e]
"abcde"

Lastly, there is the idea of an iolist which provides an efficient way to build up content for efficient output and is used in lots of places including Phoenix. Having String.Chars support iolists makes it easy to take this data structure and form an Elixir string if required.

2 Likes