Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking if symbol is present in the array with include?

Tags:

symbols

ruby

Trying Ruby with the help of Ruby Koans. There is following test there:

def test_method_names_become_symbols
  symbols_as_strings = Symbol.all_symbols.map { |x| x.to_s }
  assert_equal __, symbols_as_strings.include?("test_method_names_become_symbols")
end

# THINK ABOUT IT:
#
# Why do we convert the list of symbols to strings and then compare
# against the string value rather than against symbols?

I tried to do the same thing in the irb console and it returned false for the undefined method. But then I tried the same in some test.rb file and true was returned to both existing and unexisting methods.

Sample code:

def test_method
end
symbols = Symbol.all_symbols.map { |x| x }
puts symbols.include?(:test_method) # returns true in both cases
puts symbols.include?(:test_method_nonexistant) # returns false in irb, true if executed directly

The questions are: why do we convert symbols to strings in such case and why there are different results in irb and normal file?

Thanks!

like image 636
Ilya Tsuryev Avatar asked Dec 17 '22 07:12

Ilya Tsuryev


2 Answers

Let us go through a slightly modified version of your test code as it is seen by irb and as a stand alone script:

def test_method;end
symbols = Symbol.all_symbols # This is already a "fixed" array, no need for map
puts symbols.include?(:test_method)
puts symbols.include?('test_method_nonexistent'.to_sym)
puts symbols.include?(:test_method_nonexistent)
eval 'puts symbols.include?(:really_not_there)'

When you try this in irb, each line will be parsed and evaluated before the next line. When you hit the second line, symbols will contain :test_method because def test_method;end has already been evaluated. But, the :test_method_nonexistent symbol hasn't been seen anywhere when we hit line 2 so lines 4 and 5 will say "false". Line 6 will, of course, give us another false because :really_not_there doesn't exist until after eval returns. So irb says this:

true
false
false
false

If you run this as a Ruby script, things happen in a slightly different order. First Ruby will parse the script into an internal format that the Ruby VM understands and then it goes back to the first line and starts executing the script. When the script is being parsed, the :test_method symbol will exist after the first line is parsed and :test_method_nonexistent will exist after the fifth line has been parsed; so, before the script runs, two of the symbols we're interested in are known. When we hit line six, Ruby just sees an eval and a string but it doesn't yet know that the eval cause a symbol to come into existence.

Now we have two of our symbols (:test_method and :test_method_nonexistent) and a simple string that, when fed to eval, will create a symbol (:really_not_there). Then we go back to the beginning and the VM starts running code. When we run line 2 and cache our symbols array, both :test_method and :test_method_nonexistent will exist and appear in the symbols array because the parser created them. So lines 3 through 5:

puts symbols.include?(:test_method)
puts symbols.include?('test_method_nonexistent'.to_sym)
puts symbols.include?(:test_method_nonexistent)

will print "true". Then we hit line 6:

eval 'puts symbols.include?(:really_not_there)'

and "false" is printed because :really_not_there is created by the eval at run-time rather than during parsing. The result is that Ruby says:

true
true
true
false

If we add this at the end:

symbols = Symbol.all_symbols
puts symbols.include?('really_not_there'.to_sym)

Then we'll get another "true" out of both irb and the stand-alone script because eval will have created :really_not_there and we will have grabbed a fresh copy of the symbol list.

like image 122
mu is too short Avatar answered Feb 11 '23 06:02

mu is too short


The reason you have to convert symbols to strings when checking for existence of a symbol is that it will always return true otherwise. The argument being passed to the include? method gets evaluated first, so if you pass it a symbol, the a new symbol is instantiated and added into the heap, so Symbol.all_symbols does, in fact, have a copy of the symbol.

Symbol.all_symbols.include? :the_crow_flies_at_midnight #=> true

However, converting thousands of symbols to strings for comparison (which is much faster with symbols) is a poor solution. A better method would be to change the order that these statements get evaluated:

symbols = Symbol.all_symbols
symbols.include? :the_crow_flies_at_midnight #=> false 

This "snapshot" of what symbols are in the dictionary is taken before our tested symbol is inserted onto the heap, so even though our argument exists on the heap at the time the include? method is called, we are getting the result we expect.

I don't know why it isn't working in your IRB console. Perhaps you mistyped.

like image 26
coreyward Avatar answered Feb 11 '23 07:02

coreyward