An atom is basically an integer, nothing more. The runtime has mappings between these integers and a string value, but all comparisons are done on these integer values (literally an integer too if you look at how erlang encodes them at runtime, there is an atom map to do the translations at compile time that things like binary_to_atom look into).
In C parlance it would be like a global:
enum Atoms {
List,
All,
Atoms,
Here,
}
const char *getStringOfAtom(Atom atom) { ... }
In C++ it would be more accurately a flyweightâd string:
typedef flyweight<std::string> Atom;
Atom someAtom("SomeAtom");
// Use someAtom, compare it, etc..., it is actually a handle into a global atom map,
// which in this case does get 'collected' when all atoms go out of scope because of
// RAII, which BEAM/EVM does not do for efficiency. But you can get the string back
// from it, compare it fast, whatever...
Although I made a library a while back when C++11 came out that Iâve been using since to give me atom-like things in C++ without any of the runtime or GC or memory costs, except it limits me to a set amount of characters of a maximum length, certainly not as generic as erlangâs atoms, but hey, I can even switch on them (since they are just integers under-the-hood)! The code I have a copy of in my OverECS example project: https://github.com/OvermindDL1/OverECS/blob/master/StringAtom.hpp
using namespace OverLib::StringAtom;
Atom64 atom{}; // A default-allocated atom is just the empty string ""
atom = "SomeAtom"_atom64; // The "SomeAtom" string as an atom, this happens at compile-time
atom = atomize64("SomeAtom"); // The "SomeAtom" string as an atom, happens at run-time
std::string atomString = deatomize64(atom); // Get the string that the atom represents, this only happens at compile-time
// Yes this works! And was the original motivating reason too.
switch(atom) {
case "AnAtom"_atom64: blah(); break;
case "SomeAtom"_atom64: blorp(); break;
default: bloop();
}
I used those to great effect in a lot of systems. It is just a simple 5 or 10 char -> integer mapping via a mapping table with optional loose(default) or tight encodings, the tight gives you a few extra chars of length in exchange for forcing case-insensitivity. Usually I use flyweight strings for longer âinternedâ strings that allow for pointer comparisons, which are fast, but when the overhead is too much or I want to store in less space, my atomâs have been awesome. For example I pass around events in some of my projects like this:
void handleEvent(VariantMap event) {
// This mapping is done at compile-time so it just becomes a quick integer lookup:
float tick = event["DELTA"_atom64].get<float>(0.0f);
// This mapping is done at run-time, but still fast:
float tick = event["DELTA"].get<float>(0.0f);
// Though for generic things like a DELTA call I actually have a global helper type that does the casting/default/andAllElse:
float tick = event[DELTA];
}
That is also exposed to LUA and the usage inside a VariantMap makes it very easy to use and make events from inside LUA:
local function handleEvent(event)
-- Dynamic access, still pretty fast actually
tick = event["DELTA"]
-- Using a registered deserialization object, much faster, but does not really matter overall
tick = event[DELTA]
end
And of course, OCaml has built-in âatomsâ:
let someAtom = `GlobalAtom
let anotherAtom = `AnotherAtom
Except in OCaml you can also attach additional data to its âatomâ (polymorphic variant), basically like a tagged tuple in erlang:
let something = `GlobalAtom 42
A given atom without data and an atom with data are two different atom types and will not match, say like this:
`Ok (42, "string")
(* Does not match: *)
`Ok 42
(* Does not match: *)
`Ok
Just like:
{:ok, 42, "string"}
# Does not match:
{:ok, 42}
# Does not match:
:ok
Though like in erlang/elixir you can test that it âisâ a polymorphic variant, then refine on it if want to get data or not.
So in essence an atom is anything for whatever the context wants it to be.