18

tl;dr: Would there be a language-agnostic definition of Symbols and a reason to have them in other languages?

So, why did the Ruby creator use the concept of symbols in the language?

I ask this from the perspective of a non-ruby programmer. I've learned lots of other languages, and found in none of them, the need to specify if I was dealing or not with what Ruby calls symbols.

The main question is, does the concept of symbols in Ruby exist for performance, or just something that is needed because of the way the language is written?

Would a program in Ruby be lighter and/or faster than its, let's say, Python or Javascript counterpart? If so, would it be because of symbols?

Since one of Ruby's intent is to be easy to read and write for humans, couldn't have its creators eased the process of coding by implementing those improvements in the interpreter itself (as it might be in other languages)?

Looks like everybody wants to know only what symbols are and how to use them, and not why they are there in the first place.

1
  • 2
    Scala has Symbols, off the top of my head. I think many Lisps do. Commented May 21, 2019 at 16:44

3 Answers 3

19

The creator of Ruby, Yukihiro "Matz" Matsumoto, posted an explanation about how Ruby was influenced by Lisp, Smalltalk, Perl (and Wikipedia says Ada and Eiffel too):

Ruby is a language designed in the following steps:

  • take a simple lisp language (like one prior to CL).
  • remove macros, s-expression.
  • add simple object system (much simpler than CLOS).
  • add blocks, inspired by higher order functions.
  • add methods found in Smalltalk.
  • add functionality found in Perl (in OO way).

So, Ruby was a Lisp originally, in theory.

Let's call it MatzLisp from now on. ;-)

In any compiler, you are going to manage identifiers for functions, variables, named blocks, types, and so on. Typically you store them in the compiler and forget about them in the produced executable, except when you add debugging information.

In Lisp, such symbols are first-class resources, hosted in different packages, which mean you can add fresh symbols at runtime, bind them to different kind of objects. This is useful when meta-programming because you can be sure you won't have naming collisions with other parts of the code.

Also, symbols are interned at read time and can be compared by identity, which is an efficient way to have new kind of values (like numbers, but abstract). This help writing code where you use symbolic values directly, instead of defining your own enum types backed by integers. Also, each symbol can hold additional data. That's how, for example, Emacs/Slime can attach metadata from Emacs right into a symbol's property list.

The notion of symbol is central in Lisp. Have a look for example at PAIP (Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp, Norvig) for detailed examples.

1
  • 8
    Good answer. However I disagree with Matz: I would never think of calling a language without macros a lisp dialect. The runtime-metaprogramming facilities of lisp are precisely the thing that gives this language its awesome power, making up for its abysmally simplistic, inexpressive grammar. Commented Sep 15, 2016 at 17:58
12

So, why did Ruby creators had to use the concept of symbols in the language?

Well, they didn't strictly "have to", they chose to. Also, note that strictly speaking Symbols are not part of the language, they are part of the core library. They do have language-level literal syntax, but they would work just as well if you had to construct them by calling Symbol::new.

I ask from the perspective of a non-ruby programmer trying to understand it. I've learned lots of other languages and found in none of them the need to specify if I was dealing or not with what Ruby calls symbols.

You didn't say what those "lots of other languages" are, but here's just a small excerpt of languages that have a Symbol datatype like Ruby's:

There are also other languages which provide the features of Symbols in a different form. In Java, for example, the features of Ruby's Strings are split into two (actually three) types: String and StringBuilder/StringBuffer. On the other hand, the features of Ruby's Symbol type are folded into the Java String type: Java Strings can be interned, literal strings and Strings which are the result of compile-time evaluated constant expressions are automatically interned, dynamically generated Strings can be interned by calling the String.intern method. An interned String in Java is exactly like a Symbol in Ruby, but it's not implemented as a separate type, it's just a different state that a Java String can be in. (Note: in earlier versions of Ruby, String#to_sym used to be called String#intern and that method still exists today as a legacy alias.)

The main question could be: Does the concept of symbols in Ruby exists as an performance intent over itself and other languages,

Symbols are first and foremost a datatype with specific semantics. These semantics also make it possible to implement some performant operations (e.g. fast O(1) equality testing), but that's not the main purpose.

or just something that is needed to exist because of the way the language is written?

Symbols are not needed in the Ruby language at all, Ruby would work just fine without them. They are purely a library feature. There is exactly one place in the language that is tied to Symbols: a def method definition expression evaluates to a Symbol denoting the name of the method that is being defined. However, that is a rather recent change, before that, the return value was simply left unspecified. MRI simply evaluated to nil, Rubinius evaluated to a Rubinius::CompiledMethod object, and so on. It would also be possible to evaluate to an UnboundMethod … or just a String.

Would a program in Ruby be lighter and/or faster than its, lets say, Python or Node counterpart? If so, would it be because of symbols?

I'm not sure what you are asking here. Performance is mostly a matter of implementation quality, not language. Plus, Node isn't even a language, it's an evented I/O framework for ECMAScript. Running an equivalent script on IronPython and MRI, IronPython is likely to be faster. Running an equivalent script on CPython and JRuby+Truffle, JRuby+Truffle is likely to be faster. This has nothing to do with Symbols but with the quality of the implementation: JRuby+Truffle has an aggressively optimizing compiler, plus the whole optimization machinery of a high-performance JVM, CPython is a simple interpreter.

Since one of Ruby's intent is to be easy to read and write for humans, couldn't its creators ease the process of coding by implementing those improvements in the interpreter itself (as it might be in other languages)?

No. Symbols are not a compiler optimization. They are a separate datatype with specific semantics. They are not like YARV's flonums, which are a private internal optimization for Floats. The situation is not the same as for Integer, Bignum and Fixnum, which should be an invisible private internal optimization detail, but unfortunately isn't. (This is finally going to be fixed in Ruby 2.4, which removes Fixnum and Bignum and leaves just Integer.)

Doing it the way Java does it, as a special state of normal Strings means that you always need to be wary about whether or not your Strings are in that special state and under which circumstances they are automatically in that special state and when not. That's a much higher burden than simply having a separate datatype.

Would there be a language-agnostic definition of Symbols and a reason to have them in other languages?

Symbol is a datatype that denotes the concept of name or label. Symbols are value objects, immutable, usually immediate (if the language distinguishes such a thing), stateless, and have no identity. Two Symbols wich are equal are also guaranteed to be identical, in other words, two Symbols which are equal are actually the same one Symbol. This means that value equality and reference equality are the same thing, and thus equality is efficient and O(1).

The reasons to have them in a language are really the same, independent of the language. Some languages rely more on them than others.

In the Lisp family, for example, there is no concept of "variable". Instead, you have Symbols associated to values.

In languages with reflective or introspective capabilities, Symbols are often used to denote the names of reflected entities in the reflection APIs, e.g. in Ruby, Object#methods, Object#singleton_methods, Object#public_methods, Object#protected_methods, and Object#public_methods return an Array of Symbols (although they could just as well return an Array of Methods). Object#public_send takes a Symbol denoting the name of the message to send as an argument (although it also accepts a String as well, Symbol is more semantically correct).

In ECMAScript, Symbols are a fundamental building block of making ECMAScript capability-safe in the future. They also play a big role in reflection.

1
  • 1
    Erlang atoms were taken directly from Prolog (Robert Virding told me that at some point)
    – Zachary K
    Commented May 22, 2019 at 10:25
3

Symbols are useful in Ruby, and you'll see them all over Ruby code because each symbol is reused every time it's referenced. This is a performance improvement over strings because each use of a string that's not saved in a variable creates a new object in memory. For example, if I use the same string multiple times as a hash key:

my_hash = {"a" => 1, "b" => 2, "c" => 3}
100_000.times { |i| puts my_hash["a"] }

The string "a" is created 101,000 times in memory. If I used a symbol instead:

my_hash = {a: 1, b: 2, c: 3}
100_000.times { |i| puts my_hash[:a] }

The symbol :a is still one object in memory. This makes symbols vastly more efficient than strings.

UPDATE Here's a benchmark (taken from Codecademy) that demonstrates the performance difference:

require 'benchmark'

string_AZ = Hash[("a".."z").to_a.zip((1..26).to_a)]
symbol_AZ = Hash[(:a..:z).to_a.zip((1..26).to_a)]

string_time = Benchmark.realtime do
  100_000.times { string_AZ["r"] }
end

symbol_time = Benchmark.realtime do
  100_000.times { symbol_AZ[:r] }
end

puts "String time: #{string_time} seconds."
puts "Symbol time: #{symbol_time} seconds."

Here are my results for my MBP:

String time: 0.1254125550040044 seconds.
Symbol time: 0.07360960397636518 seconds.

There's a clear difference in using strings vs. symbols for just identifying keys in a hash.

4
  • 1
    I am not sure if this is the case. I would expect a Ruby implementation to execute the same code multiple times, not parsing the code again and again for each iteration. Even if each lexical occurrence of "a" is indeed a fresh string, I think in your example there will be exactly two "a" (and an implementation could even share the memory until one of them is mutated). In order to create millions of strings, you would probably need to use String.new("a"). But I am not well versed in Ruby, so maybe I am wrong.
    – coredump
    Commented Aug 10, 2016 at 17:48
  • 1
    In one of Codecademy's lessons, they generate a benchmark for strings vs symbols, much like my example. I'll add it to the answer. Commented Aug 10, 2016 at 18:22
  • 1
    Thanks for adding the benchmark. Your test shows the expected gain obtained by using symbols instead of strings, due to faster test in the hashtable (identity vs string compare), but there is no way we can deduce that strings gets allocated at each iteration. I added a version with string_AZ[String.new("r")] in order to see if that makes a difference. I get 21ms for strings (original version), 7ms with symbols and 50ms with fresh strings each time. So I would say that strings are not allocated as much with the literal "r" version.
    – coredump
    Commented Aug 10, 2016 at 21:10
  • 2
    Ah, so I did some more digging, and in Ruby 2.1, strings are in fact shared. I apparently missed that update; thanks for pointing that out. Going back to the original question, I think both benchmarks show the utility of symbols vs strings. Commented Aug 10, 2016 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.