
Ruby symbols explained
What's a Symbol?
Symbol vs String
In Ruby, everything's an object. Symbols and strings are objects, instances of Symbol and String classes.
Strings are declared using quotes or apostrophes.
Symbols are defined by a colon before the literal.
Symbols can also contain quotes and apostrophes.
Remember, this notation denotes a Symbol, not a String.
Using the above notation, a Symbol can include spaces or other special characters. In traditional notation, an underscore is used as a separator.
Symbols can't be modified
Strings can be modified, but symbols can't. Symbols don't have bang methods that change the calling object.
Roles of symbols and strings
Symbol identifies something important, it's an object for unique identifiers.
String is designed to store words or pieces of text, it's a type for textual data.
Symbols and strings can be converted to each other
Symbol and Memory
The same string always has a different object_id, while the same symbol called thrice returns the same value.
This is the key feature, the meat and potatoes, the whole point of symbols in Ruby.
Each string, despite having the same value, is a separate instance with its own object_id and memory location. A Symbol, however, is unique, created only once during program execution. Every time we call a symbol with the same value, we're calling the same instance.
This impacts code efficiency. Each such string occupies additional memory.
Historical tidbit! Since Ruby 2.2.0, the Garbage Collector handles symbols. Previously, the efficiency issue between String and Symbol was tricky. Symbols were kept in memory throughout the program's runtime, which could lead to memory leaks when dynamically generating symbols, e.g., from external service responses in long-running applications. Since Ruby 2.2.0, there's no need to worry about memory with a large number of symbols. For strings, the situation remains unchanged.
To understand how the Garbage Collector handles symbols, we need to grasp the difference between explicitly declared symbols and dynamically created ones.
Explicitly Declared Symbols vs Dynamically Created Symbols
The Garbage Collector only cleaned dynamically created symbols.
The Garbage Collector implementation in Ruby 2.2.0 and higher helps defend against a phenomenon known as symbol DoS - symbol denial of service attack.
Symbol DoS occurs when a system dynamically creates many symbols, e.g., when processing a JSON response from an API. This leads to memory leaks.
Despite improvements in the Garbage Collector mechanism, creating symbols from received data will never be entirely safe, for example, when dynamically creating a method from parameters:
This symbol was dynamically created to trigger a method. When it's used further, after being destroyed by the Garbage Collector, it won't be possible to call this method anymore.
The same String, used multiple times, will be instantiated and destroyed each time, while a Symbol, occupying one fixed memory location, will use the same instance throughout the program's runtime. Similarly, a dynamically created Symbol will behave this way until marked for destruction by the Garbage Collector.
Efficiency in equality operations
Memory issues become apparent when comparing Strings and Symbols. Since strings are mutable and each has its own object_id, Ruby never really knows a String's value, so it must check character by character. When comparing two Strings, Ruby must check both character sequences and compare if they have the same values and order. Comparing object identifiers will give a false result, as two identical strings with the same value have different object_ids.
Comparing Symbols is entirely different. Symbols are immutable. Two symbols with the same value have the same object_id. Due to this immutability, Ruby is certain that a Symbol hasn't changed during the program's runtime, so there's no need to compare character sequences. A quick comparison of object identifiers suffices. The whole operation happens much faster than with strings.
Let the numbers speak for themselves.
Storing Symbols
There’s one more efficiency aspect worth noting. All symbols are stored in memory in a single array.
We can access it using:
How does this affect efficiency? Each time the same String is called, it's instantiated and then destroyed after use.
With symbols, Ruby first checks the mentioned global array. If it finds the requested symbol there, it returns it immediately. If not, only then does it create a new instance, which is also placed in the symbol array and will be returned faster next time because it's already available there. Symbols are efficient when it comes to comparing, storing, and using.
Simulating Symbols
Ruby provides the ability to simulate Symbol behavior on a String by calling the freeze
method. This makes the string an immutable constant. However, this doesn't translate to memory. Two frozen strings are still two different instances. A frozen object, like a Symbol, cannot be modified. It's instantiated only once for each individual instance, then it's called for use.
Fun fact!
Symbol, fixnum, bignum, float are frozen by default. When using frozen strings as keys in a Hash, we can achieve better code efficiency than with symbols.
Where to use Symbols?
There's no clear definition of when to use Strings and when to use Symbols.
The most popular example of symbol usage is as keys in Hashes.
The allowed notation is:
or
The second example presents the currently accepted new notation.
Why is it better to use symbols as keys?
Because they represent unique values that aren't static, while strings are default containers for data. Therefore, a Symbol better fits the role of an identifier. As is the case with keys in a Hash. If we use a String as a key, Ruby will create a new instance each time it's called. With symbols, the same object will be used.
If we have a declared Hash with symbols as keys, we can't access them using strings, and vice versa.
In Ruby on Rails, there's an object that allows accessing values using both symbols and strings on the same Hash.
HashWithIndifferentAccess
This object is HashWithIndifferentAccess. Using symbols with this object is much faster than using a regular Hash.
Symbols are also keywords for method arguments.
Symbols are used to define attributes in classes.
Symbols represent instances of classes or methods
When declaring a class, method, or variable, Ruby automatically creates its name as a Symbol, hence the following notations are possible:
Interestingly, if we have a class called Controller, a method controller, and a variable controller, even though in different contexts this name will refer to different values, the same object will be called each time.
Symbols in enum in Ruby on Rails
In Ruby on Rails, there's an enum object where symbols are used to define allowable values for particular fields.
What's worth remembering?
I've encountered cases of confusing symbols with variables, which is an egregious error. A Symbol is not a variable, but a name, or in other words, a named object.
It's crucial to remember that Symbol is a special class in Ruby used to define immutable, unique names.
Variables, on the other hand, are names that refer to objects. When an object ceases to exist - the variable also ceases to exist.
It's different with a Symbol. It exists even if the object it names doesn't exist.
A Symbol is not a variable - you can't assign it a value.
Another interesting thing is that although a Symbol is not a String, it has a string representation which is constant. In the case where we have a Symbol written with quotation marks, its string representation will also have spaces.
However, symbols with spaces are not commonly used and, when there's no other way to write them, they're used as a last resort.
A Few Simple Rules
Finally, here are a few simple rules for when to use Strings and when to use Symbols, in case it's still problematic. So, in addition to the above examples, remember that:
Symbols are very noble objects in the Ruby world. Using them skillfully gives only advantages, from code readability to efficiency and memory savings.
For more details, I refer you to the Symbol class documentation: https://ruby-doc.org/core-2.7.1/Symbol.html