Использование атомов из объявленных структур в elixir работает на repl, но не в приложении

Я хочу использовать String.to_existing_atom в эликсире, чтобы избежать утечек памяти.

Это 100% работает на REPL:

iex(1)> defmodule MyModule do
...(1)> defstruct my_crazy_atom: nil
...(1)> end
{:module, MyModule,
 <<70, 79, 82, ...>>,
 %MyModule{my_crazy_atom: nil}}

Итак, теперь атом my_crazy_atom существует. Я могу это проверить:

iex(2)> String.to_existing_atom "my_crazy_atom"
:my_crazy_atom

В сравнении с:

iex(3)> String.to_existing_atom "my_crazy_atom2"
** (ArgumentError) argument error
    :erlang.binary_to_existing_atom("my_crazy_atom2", :utf8)

Но у меня есть код, который выглядит так:

defmodule Broadcast.Config.File do
  defstruct channel_id: nil, parser: nil
end

Из вызова метода после запуска процесса GenServer я могу декодировать с помощью Poison's

keys: :atoms! 

или даже просто позвонить

String.to_existing_atom("parser")

там же в коде и получаю ошибку:

** (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)

Как ни странно, если я создам экземпляр структуры и проверю ее, проблема исчезнет!

IO.puts inspect %Broadcast.Config.File{}
String.to_existing_atom("parser")

Что тут происходит? Это что-то типа заказа?


person JasonG    schedule 20.09.2017    source источник
comment
Можете ли вы опубликовать полный код, который воспроизводит ошибку? Я не могу воспроизвести ошибку, скопировав MyModule, а затем выполнив String.to_existing_atom("my_crazy_atom") из другой функции в другом модуле.   -  person Dogbert    schedule 20.09.2017
comment
Попробовать в проекте? Думаю, у @mudasobwa есть ответ.   -  person JasonG    schedule 20.09.2017
comment
Я поместил код в смешанный проект. Где я должен поместить вызов String.to_existing_atom? Если я помещу его в функцию, а затем вызову, она будет работать без необходимости создавать экземпляр структуры. (Я не думаю, что ответ мудасобвы правильный (или, может быть, я неправильно понял вопрос.))   -  person Dogbert    schedule 20.09.2017
comment
@Dogbert, пожалуйста, посмотрите обновление в моем ответе, чтобы узнать, как воспроизвести.   -  person Aleksei Matiushkin    schedule 20.09.2017
comment
@mudasobwa спасибо, теперь я могу воспроизвести ошибку. :)   -  person Dogbert    schedule 20.09.2017


Ответы (2)


Это происходит потому, что Elixir по умолчанию лениво загружает модули из скомпилированных файлов .beam при их первом использовании. (Ваш код будет работать, если start_permanent установлено в true в mix.exs, для которого по умолчанию установлено значение true в среде :prod, потому что тогда Elixir жадно загружает все модули пакета.)

В приведенном ниже коде атом :my_crazy_atom будет присутствовать в коде модуля Blah, но его нет в Foo. Если вы запустите сеанс REPL и запустите Foo.to_existing_atom, модуль Blah не будет загружен, что приведет к сбою String.to_existing_atom("my_crazy_atom").

# 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

Как вы заметили, если вы однажды создадите структуру вручную, все последующие вызовы String.to_existing_atom("my_crazy_atom") вернут правильный атом. Это связано с тем, что когда вы создаете структуру, Elixir загрузит файл .beam этого модуля, который также загрузит все атомы, используемые этим модулем.

Лучший способ загрузить модуль (по сравнению с созданием структуры) — использовать Code.ensure_loaded/1 для загрузки модуля:

{:module, _} = Code.ensure_loaded(Blah)
person Dogbert    schedule 20.09.2017
comment
Спасибо Догберт. Мне пришлось передать это @mudasobwa, так как он ответил первым :) Цените ответ и лайкайте решение с Code.ensure_loaded - person JasonG; 21.09.2017
comment
Я думаю, что в ответе mudaobwa отсутствует точная причина, по которой это происходит: ленивая загрузка модулей. Если вы запустите Elixir с start_permanent: true (по умолчанию для prod env), вы увидите, что все работает, даже если вы не создаете экземпляр структуры. Единственная причина, по которой ваш обходной путь (создание экземпляра структуры) сработал, заключается в том, что при создании структуры Эликсир загружает этот модуль, что приводит к созданию всех атомов, используемых этим модулем. - person Dogbert; 21.09.2017
comment
@JasonG FWIW, я сам проголосовал за этот ответ, и я думаю, вы должны отметить его как правильный. - person Aleksei Matiushkin; 21.09.2017

Разница между REPL и вашим приложением заключается в том, что в REPL процесс компиляции происходит немедленно. Тем не менее, ВМ, как она видит

iex(1)> defmodule MyModule do
...(1)>   defstruct my_crazy_atom: nil
...(1)> end

он сразу скомпилирован. На этапе компиляции создается атом и все работает.

В вашем приложении OTOH процесс компиляции выполняется заранее путем другого вызова виртуальной машины. Следовательно, если структура не используется явно, атом не будет создан.

Можно подумать об этом как об объявлении класса по сравнению с созданием экземпляра в ООП: существование определения класса не гарантирует наличие экземпляров этого класса.

Чтобы увидеть, что и когда на самом деле происходит, попробуйте поместить IO.puts "I AM HERE" в объявление модуля непосредственно перед defstruct. В REPL вы сразу же увидите эту строку. В вашем приложении вы увидите его во время компиляции и не увидите при обычном запуске приложения.


Действия по воспроизведению:

$ mix new blah && cd blah
$ cat lib/blah.ex
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
$ mix compile
$ iex -S mix
iex|1 ▶ Foo.to_existing_atom
** (ArgumentError) argument error
    :erlang.binary_to_existing_atom("my_crazy_atom", :utf8)
    (blah) lib/blah.ex:6: Foo.to_existing_atom/0
iex|1 ▶ %Blah{}
%Blah{my_crazy_atom: nil}
iex|2 ▶ Foo.to_existing_atom
:my_crazy_atom

Я помещу его здесь для полноты (взято из этот блестящий ответ):

defmodule AtomLookUp do
  defp atom_by_number(n),
    do: :erlang.binary_to_term(<<131, 75, n::24>>)

  def atoms(n \\ 0) do
    try do
      [atom_by_number(n) | atoms(n + 1)]
    rescue
      _ -> []
    end
  end
  def atom?(value) when is_binary(value) do
    result = atoms()
             |> Enum.map(&Atom.to_string/1)
             |> Enum.find(& &1 == value)
    if result, do: String.to_existing_atom(result)
  end
end

iex|1 ▶ AtomLookUp.atom? "my_crazy_atom"
nil
iex|2 ▶ %Blah{}
%Blah{my_crazy_atom: nil}
iex|3 ▶ AtomLookUp.atom? "my_crazy_atom"
:my_crazy_atom
person Aleksei Matiushkin    schedule 20.09.2017
comment
Спасибо, так что здесь идиоматический подход - я просто вызываю структуры один раз и отбрасываю их, чтобы убедиться, что атомы созданы, что кажется неправильным. - person JasonG; 20.09.2017
comment
Честно говоря, я понятия не имею. Я бы, вероятно, пошел с объявлением списка разрешенных атомов (которые обычно едва ли строго равны полям всех структур проекта) и выполнением чего-то вроде MyAtoms.permitted перед запуском приложения. - person Aleksei Matiushkin; 20.09.2017