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!
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With