Is there a way to check if atom exists other than rescue ArgumentError on binary_to_existing_atom?

I am doing something like:

mod = ["Some", "Module"]
try do
  Module.safe_concat(mod)
rescue
  ArgumentError -> nil
end

but I don’t like it. Remind me a java days :wink: Any other options?

1 Like

Since that raises (Same with String.to_existing_atom/1, something I can think of (I’m assuming you’re using try...do/rescue) is to send the values to another function and use the built-in syntax for that rescue.

def something do
  ...
  handle_value(value)
end

def handle_value(value) do
  # Handles value
rescue
  ArgumentError -> nil
end

Unless you want to use the rescue on the first function, that’s up to you honestly.

Well, that’s exactly the same. What I wanted to avoid here, is catching the exception. It is just a matter of taste, because obviously this code works perfectly, but looks ugly :slight_smile:

Module.safe_concat/2

Concatenates a list of aliases and returns a new alias only if the alias was already referenced.

So even if the atoms already exist, an ArgumentError could still be raised.

1 Like

Interesting!
What “alias was already referenced” means and how it is different than “atom exists”?

I took a look into the source and I can’t see when it could be the case. Now it looks like the only requirement is the atom exists.

iex(1)> String.to_atom("Elixir.A.B")
A.B
iex(2)> Module.safe_concat("A", "B")
A.B

But I understand it might change in the future, and safe_concat might check if the alias was referenced. In my case it is perfectly OK.

I think you should do a validation beforehand.

defmodule SafeAtomConcat do
  def concat(mod) do
    with {:ok, mod} <- mod_contains_atoms?(mod) do
      Module.safe_concat(mod)
    else
      {:error, msg} -> msg
    end
  end

  def mod_contains_atoms?(mod) do
    case Enum.all?(mod, &is_atom/1) do
      true -> {:ok, mod}
      false -> {:error, "Mod contains non-atom data."}
    end
  end
end

with is there just in case you need more validations in the future, but if you don’t, a simple conditional would suffice.

case Enum.all?(mod, &is_atom/1) do
  true -> Module.safe_concat(mod)
  false -> {:error, "Mod contains non-atom data."}
end

I can’t do like this.
Data comes as a list of binaries, and after concatenation if might be a proper, existing alias, but don’t have to.

My code is just an example, you can change the code on the false part. Other than that, I could not think any other solution besides using your exception above.

This is not about the false part. Your code checks if all the list members are atoms, which is not my case.

Let’s ask the same question differently: how to check if the string is existing atom, without catching an exception?

string = "is_it_an_existing_atom?"
atom = try do
  String.to_existing_atom(string)
rescue
  ArgumentError -> {:nope}
end

Something like this?

:erlang.function_exported(List.Chars, :module_info, 0)

you have to convert the list into atom though.

Yes, something like this.

:the_mythical_erlang_dialect.atom_exists("is_it_an_existing_atom?")

The function that takes a binary string and returns true, if the atom exists. Without catching the exception :slight_smile:

What do you mean by that?

@grych I think the usual answer is “The reason this doesn’t feel ergonomic is because this isn’t an idiomatic approach”. If an input has a known structure, then you work from the known structure and pull out values. So for example if you have parsed JSON and you want to try to pull that into a struct you can do:

struct = %SomeModule{}
json = %{...}

struct
|> Map.from_struct
|> Enum.reduce(struct, fn {k, v}, struct ->
  string_key = Atom.to_string(k)
  Map.put(struct, k, Map.get(json, string_key, v))
end)

No catching or anything is necessary cause we’re working our way from the atom side over to the string side and not the other way around.

2 Likes

This code determine whether the atom List.Chars exists, without exception. It returns true or false.

:erlang.function_exported(List.Chars, :module_info, 0)

Nope.
This code checks if the public function List.Chars.module_info/0 exists.
When you call it, List.Chars atom must exist, because you just used it as an argument! :slight_smile:

I totally agree.
Anyway, looks like I have to live with it.

In my case, I need to get the actual module alias, based on the different module name. The similar way how Phoenix gets PageController and turns it into PageView. In Phoenix, PageView module must exists, so they just can do String.to_existing_atom. In my software, it is allowed to not to declare my kind of PageView.

Let’s ask the same question differently: how to check if the string is existing atom, without catching an exception?

I can only think of a silly approach, read all existing atoms in a list and lookup in there.

atoms_count = :erlang.system_info(:atom_count)
existing_atoms = Enum.map(0..atoms_count - 1, fn i ->
  atom = :erlang.binary_to_term(<<131, 75, i::24>>)
  {to_string(atom), atom}
end)

my_wannabe_atom = "error"
:proplists.get_value(my_wannabe_atom, existing_atoms, nil) # returns :error

my_wannabe_atom = "asjdkfhgasd"
:proplists.get_value(my_wannabe_atom, existing_atoms, nil) # returns nil (likely)

# but!
my_wannabe_atom = "nil"
1 Like

The requirement is that the atom equivalent to the string after successful concatenation exists. I was left with the superficial impression that you were going to trying to check for the existence of :Some and then :Module - my mistake.

Anyway, looks like I have to live with it.

It’s probably less work if you just live with it.

In my software, it is allowed to not to declare my kind of PageView.

Ben’s approach could still apply (provided it’s worth the work):

  • if you have a means for identifying the eligible module(-function)s (during compile time?)
  • then you may be able to generate a “string to atom” Map
  • and use Map.fetch/2 during run-time to do the string to atom mapping.
2 Likes

This is a nice approach, but in my case probably not worth the hassle. Thanks anyway! It is always good to learn something new.

I may be late, but why don’t just wrap that ugly part into a new module since there’s no built in functions that meet your need? I think this is a simpler approach.

It is still ugly, despite of the module it lives in.

1 Like