DEV Community

Cover image for The symbol in Ruby - Explanation
Rafał Piekara
Rafał Piekara

Posted on • Updated on

The symbol in Ruby - Explanation

Everybody knows how to use it, everybody knows where, but why and what is its basis? What is a symbol in Ruby - I explain to myself and everyone who used it unknowingly so far. A few years ago, when I moved from .Net and C # to the Ruby world, I initially had trouble understanding what the symbols really were about. In one of the interviews, I got a question about the difference between a Symbol and a String and couldn't explain it. After total freaking out, I decided to explore the topic and now I am sharing with you the knowledge that I gained once and for all.

What is a Symbol?

  • an immutable object that is the name of another object
  • unique for each name
  • not associated with a specific instance of a given name while the program is running
  • is a string that cannot be changed

Symbol vs String

In Ruby, everything is an object, symbols and strings are objects, and instances of the Symbol class and the String class.

The string is declared using double or single quotation marks.

"string" 'string'
Enter fullscreen mode Exit fullscreen mode

The symbol is defined by a colon before the literal.

:symbol
Enter fullscreen mode Exit fullscreen mode

Symbols can also contain quotation marks and apostrophes

:"symbol" :'symbol'
Enter fullscreen mode Exit fullscreen mode

Remember that such an entry means Symbol, not String.

If we use the above notation, the symbol may contain spaces or other special characters. In traditional notation, underscore is used as the separator.

Symbols cannot be modified

Strings can be modified, symbols are not. Symbols do not have bang methods that change the object that calls them.

"string".upcase #=> "STRING"

:symbol.upcase #=> :SYMBOL

"string".upcase! #=> "STRING"

:symbol.upcase!
NoMethodError: undefined method 'upcase!' for :symbol:Symbol

str = "string"
str = str << " sample" #=> "string sample"

sym = :symbol
sym = sym << " sample" #=> NoMethodError: unfined method '<<' for :symbol:Symbol
sym = sym << :sampel #=> NoMethodError: unfined method '<<' for :symbol:Symbol
Enter fullscreen mode Exit fullscreen mode

The role of symbols and strings

The symbol identifies something important, it is an object for unique identifiers.

The string is intended for storing words or pieces of text, it is a type dedicated for text data.

Symbols and strings can be converted to each other

"string".to_sym #=> :string

:symbol.to_s #=> "string"
Enter fullscreen mode Exit fullscreen mode

Symbol and memory

The_symbol_in_Ruby

The same string has a different object_id every time, while the same symbol invoked three times returns the same value.

This is a key feature, plump meat, sweet flesh, a whole lot of symbols in Ruby.

Each string, although having the same value, is a separate instance, has its own object_id and its own memory location. The symbol, on the other hand, is unique, created only once while the program is running. Every time a symbol with the same value is called, one and the same instance is called.

This has an impact on code performance. Each such string takes additional memory space.

Historical curiosity! As of Ruby 2.2.0, Garbage Collector deals with symbols. Previously, the performance issue between String and Symbol was tricky. Namely, the symbols were stored in memory for the entire duration of the program, which in the case of dynamic symbol generation, e.g. from responses from external services in the application running without restart, for a long time led to a memory leak. As of Ruby 2.2.0 there is no need to worry about memory with numerous symbols. In the case of strings, the situation remains unchanged.

To understand how Garbage Collector deals with symbols, you need to understand the difference between explicitly declared symbols and dynamically created symbols.

Explicitly Declared Symbols vs Dynamically Created Symbols

:symbol #=> deklaracja
"symbol".to_sym #=> dynamiczna kreacja
Enter fullscreen mode Exit fullscreen mode

Garbage Collector only cleaned up dynamically created symbols.

The_symbol_in_Ruby

Garbage Collector implementation in Ruby 2.2.0 and higher allows defending against the phenomenon called DoS symbol - denial of service attack symbol.

A Dos symbol is when the system dynamically creates a lot of symbols, eg by processing the Json response from an API. Memory is leaking.

Despite the improvement of the Garbage Collector mechanism, creating symbols from the received data will never be completely safe, e.g. when creating a method dynamically from parameters:

define_method(params[:method].to_sym) do
end
Enter fullscreen mode Exit fullscreen mode

This symbol was dynamically created to trigger the method. When it is further used, after being destroyed by the Garbage Collector, it will no longer be possible to call this method.

The same String, used several times, will be instantiated and destroyed each time, and Symbol, because it occupies one, permanent place in memory, will use the same instance for the entire duration of the program. It is similar to the case of a symbol created dynamically until it is marked for destruction by the Garbage Collector.

The efficiency with equality operators

Memory issues become apparent when comparing Strings and Symbols. Since strings are variable and each string has an object_id, Ruby never really knows what the value of a particular String is, so it has to check character by character. When comparing two Strings, Ruby must check both sequences of characters to see if they have the same values and if they are arranged in the same order. Comparing object identifiers will produce a skewed result because two identical strings with the same value have different object_id.

Comparing the Symbols is completely different. The symbols do not change. Two symbols with the same value have the same object_id. Due to this immutability, Ruby is sure that the Symbol has not changed over the course of the program, so there is no need to compare character sequences. All you need is a quick comparison of object identifiers. The whole operation happens much faster than with strings.

Let the numbers speak for themselves…

require 'benchmark'

str = Benchmark.measure do
        1_000_000.times do
          "string"
        end
      end.total #=> 0.07

sym = Benchmark.measure do
          1_000_000.times do
          :symbol
          end
      end.total #=> 0.39999999999999994

string_comparison = Benchmark.measure do
                        1_000_000.times do
                            "string" == "string"
                        end
                    end.total #=> 0.13

symbol_comparison = Benchmark.measure do
                        1_000_000.times do
                            :symbol == :symbol
                        end
                    end.total #=> 0.4999999999999999
Enter fullscreen mode Exit fullscreen mode

Storage of Symbols

There is one more performance aspect worth noting. All symbols are stored in memory in one collection.

We have access to it via:

Symbol.all_symbols
Enter fullscreen mode Exit fullscreen mode

How does this impact the performance? Every time the same String is called, it is instantiated and destroyed after use.

When calling symbols, it looks like Ruby checks the global collection first, and if it finds the symbol there, returns it immediately. If it does not find it, then it creates a new instance, also placed in the symbol collection, and returns it faster the next time, because it will be available there. Symbols are efficient to compare, store, and use.

Symbol Simulation

Ruby gives you the ability to simulate the behaviour of a Symbol on a String by calling the freeze method. This makes string an invariant constant. However, this does not impact the memory. Two frozen strings are still two different instances. A frozen object, like a Symbol, cannot be modified. It is instantiated only once for each individual instance, then it is called for use.

Curiosity!

Symbol, fixnum, bignum, float are frozen by default. By using frozen strings as Hash keys, we can get better code performance than with symbols.

Where to use the Symbols?

There is no clear definition of when to use Strings and when to use Symbols.

The_symbol_in_Ruby

The most common example of using symbols are Hash keys.
The allowed notation is:

old_hash = { :symbol_key => "some value" }
Enter fullscreen mode Exit fullscreen mode

or

cool_hash = { symbol_key: "some value" }
Enter fullscreen mode Exit fullscreen mode

The second example shows the currently adopted new notation.

Why is it better to use symbols as keys?

Because they present unique values that are not static, while strings are the default containers for data. Therefore, Symbol fits better as an identifier. As is the case with Hash keys. If we use a String as a key, Ruby will create a new instance each time it is invoked. In the case of symbols, one and the same object will be used.

If we have a hash declared with symbols as keys, we cannot access them with strings, the other way around is the same.

There is an object in Ruby on Rails that allows you to get to values using symbols and strings on a single Hash.

HashWithIndifferentAccess

The object is HashWithIndifferentAccess. Using symbols with this object is much faster than using a regular Hash.

hash = {} #=> {}
hash[:symbol] = 1 #=> 1
hash[:symbol] #=> 1
hash['symbol'] #=> nil

hash = HashWithIndifferentAccess.new #=> {}
hash[:symbol] = 1 #=> 1
hash[:symbol] #=> 1
hash['symbol'] #=> 1
Enter fullscreen mode Exit fullscreen mode

Symbols are also keywords for method arguments

def method_name(name:, age:)
end

method(name: "John", age: 12)
Enter fullscreen mode Exit fullscreen mode

Symbols are used to define attributes in classes

attr_accessor :name, :age
Enter fullscreen mode Exit fullscreen mode

Symbols indicate an instance of classes or methods

When declaring a class, method or variable, Ruby automatically creates its name in the form of a Symbol, hence the following entries are possible:

my_array.send(:pop)
my_array.respond_to?(:map)
Enter fullscreen mode Exit fullscreen mode

Interestingly, if we have a class called Controller, a method of controller, and a variable controller, and although in different contexts this name will refer to different values, this object will be called every time.

Symbols in enum in Ruby on Rails

There is an enum object in Ruby on Rails that uses symbols to define acceptable values for a particular field.

What is worth remembering?

I have encountered a case of confusing symbols with variables, which is a shameful mistake. The symbol is not a variable but a name or otherwise named object.

It is important to remember that Symbol is a special class in Ruby that is used to define permanent, unique names.

Variables, on the other hand, are names that refer to objects. At the moment when the object ceases to exist - the variable also ceases to exist.

Different with the Symbol. It exists even if the object it is named does not exist.

A symbol is not a variable - you cannot assign it a value.

Another interesting thing is that although the Symbol is not a String, it has a string representation which is a constant. If we have a Symbol written with quotation marks, then its string representation will also have spaces.

:"it is some symbol": "it is some symbol".to_s #=> "it is some symbol"
Enter fullscreen mode Exit fullscreen mode

However, symbols with spaces are not commonly used and when there is no other possibility of writing, they are used as a last resort.

A few simple rules

Finally, a few more simple rules when using Strings and Symbols, if that was still a problem. Therefore, in addition to the examples above, please note that:

  • when we want to display something we should choose String
  • if we want to process the name somehow, use modifying methods on it, we know that we use String
  • when the name is meant to be a unique identifier, we use Symbol
  • when the same name is to be used multiple times, we use Symbol

Symbols are very noble objects in the Ruby world. Their skilful use gives only advantages, from code readability to performance and memory savings.

For more details, please refer to the Symbol class documentation

The_symbol_in_Ruby

Top comments (0)