I want to use String.to_existing_atom in elixir to avoid memory leaks.
This 100% works on the REPL:
iex(1)> defmodule MyModule do
...(1)> defstruct my_crazy_atom: nil
...(1)> end
{:module, MyModule,
<<70, 79, 82, ...>>,
%MyModule{my_crazy_atom: nil}}
So now the atom my_crazy_atom
exists. I can verify this:
iex(2)> String.to_existing_atom "my_crazy_atom"
:my_crazy_atom
Compared to:
iex(3)> String.to_existing_atom "my_crazy_atom2"
** (ArgumentError) argument error
:erlang.binary_to_existing_atom("my_crazy_atom2", :utf8)
But I have some code that looks like this:
defmodule Broadcast.Config.File do
defstruct channel_id: nil, parser: nil
end
From a method call after starting a GenServer process, I can decode with Poison's
keys: :atoms!
or even just call
String.to_existing_atom("parser")
in the same place in the code and I get an error:
** (Mix) Could not start application broadcast: exited in:
Broadcast.Application.start(:normal, [])
** (EXIT) an exception was raised:
** (ArgumentError) argument error
:erlang.binary_to_existing_atom("parser", :utf8)
Oddly, if I instantiate the struct and inspect it, then the issue goes away!
IO.puts inspect %Broadcast.Config.File{}
String.to_existing_atom("parser")
What's going on here? Is this some kind of ordering thing?
This happens because Elixir by default lazy loads modules from the compiled .beam files when they're first used. (Your code will work if start_permanent
is set to true
in mix.exs
, which is set to true by default in the :prod
environment, because then Elixir eagerly loads all modules of the package.)
In the code below, the atom :my_crazy_atom
will be present in the Blah
module's code but it's not present in Foo
. If you start a REPL session and run Foo.to_existing_atom
, the Blah
module is not loaded which causes String.to_existing_atom("my_crazy_atom")
to fail.
# Credits: @mudasobwa
defmodule Blah do
defstruct my_crazy_atom: nil
end
defmodule Foo do
def to_existing_atom, do: String.to_existing_atom("my_crazy_atom")
end
As you've observed, if you create a struct manually once, all subsequent calls to String.to_existing_atom("my_crazy_atom")
return the correct atom. This is because when you create a struct, Elixir will load the .beam file of that module which will also load all the atoms that are used by that module.
A better way to load a module (as compared to creating a struct) is to use Code.ensure_loaded/1
to load the module:
{:module, _} = Code.ensure_loaded(Blah)