Did you know that you can call map() and each() on a Ruby string? Do you know how they behave? I hope I’m not the only one that thought they understood it but was proven mistaken.
What would you expect the following code to do?
"hello".map{|char| "Char#{char}" }
I thought it would return me an array with a bunch of “Char” where x is each letter in “hello”. Nope…it returns:
["Charhello"]
What would you expect the following code to do?
"hello".each{|char| puts "Char#{char}"}
Yeah I thought it would write out a bunch of puts statements “Char” where x is each letter in “hello”. Nope it prints:
Charhello
Ok so maybe my thoughts on strings being enumerable were a little off…I’ve been wrong before and will probably be wrong again.
What really threw me for a loop was that I can access the characters in a string by index:
"hello"[1]
returns
101
which is the ASCII representation of “e”.
So what is the moral of this story? Sometimes things aren’t as the seem at first glance. And sometimes you need to step back, fire up irb and see what really is happening.
Update: Thanks all for the responses below.
irb(main):008:0> String.instance_methods.each.select {|m| m if m.to_s.include?(‘each’)}
=> [:each_line, :each_byte, :each_char, :each_codepoint]
irb(main):009:0> “test”.each {|c| puts c}‘
NoMethodError: undefined method `each’ for “test”:String
from (irb):9
from C:/Ruby19/bin/irb:12:in `
irb(main):010:0> “test”.map {|c| c}‘
NoMethodError: undefined method `map’ for “test”:String
from (irb):10
from C:/Ruby19/bin/irb:12:in `
What version of Ruby is this on? Also is it possible that you’ve got another library which has monkey patched MethodMissing in the String class? Is this in a straight irb session or maybe from a ./script/console or rails console session? I think .each_byte or .each_char would do what you’re looking for but probably there is some kind of 3rd party intervention going on here. Good luck!
August 27, 2010 at 7:57 am
Here’s an example console output:
xxxx:~ xxxx$ gem list
*** LOCAL GEMS ***
rake (0.8.7)
rdoc (2.5.9)
xxxx:~ xxxx$ irb
1.9.2-p0 > “hello”.each{|char| puts “Char#{char}”}
Charhello
=> “hello”
1.9.2-p0 >
Also, the Ruby documentation says that [String](http://ruby-doc.org/core/classes/String.html) includes the Enumerable module so you’d expect to get each() and map(). Interesting that your version doesn’t behave the same way.
August 27, 2010 at 8:22 am
He’s using 1.8.x, you’re using 1.9.x
String does a lot of different things when going to 1.9.x.
August 27, 2010 at 8:22 am
$ irb‘
ruby-1.9.2-p0 > “hello”.each{|char| puts “Char#{char}”}
NoMethodError: undefined method `each’ for “hello”:String
from (irb):1
from /Users/jgeiger/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `
ruby-1.9.2-p0 >
Dunno why your ruby is doing that, but 1.9 doesn’t include each.
http://blog.grayproductions.net/articles/ruby_19s_string
“In Ruby 1.8, String’s each() method iterated over lines of data. I imagine that was done because it’s a common way to process data, but the question is what made lines the correct choice? What about iterating by bytes or characters? You could iterate by bytes in Ruby 1.8 using each_byte(), but you needed to resort to Regexp tricks to get characters.
In the Ruby 1.9 realm of all encoded data, blessing one type of iteration just doesn’t make sense. Instead, each() has been removed from String and it is no longer Enumerable. This is probably one of the biggest changes to the core API that code will need to adapt to.”
August 27, 2010 at 8:26 am
Looks like 1.8.7-p299 behaves the same way:
info: Using ruby 1.8.7 p299
xxxx:~ xxxx$ gem list
*** LOCAL GEMS ***
rake (0.8.7)
rdoc (2.5.9)
xxxx:~ xxxx$ irb
ruby-1.8.7-p299 > “hello”.each{|char| puts “Char#{char}”}
Charhello
=> “hello”
ruby-1.8.7-p299 >
August 27, 2010 at 8:26 am
Very interesting indeed…
JGeiger…it is very interesting that my 1.9.2-p0 seems to include each(). Thanks for the link. I’ll do a little more poking around and see what I find…
August 27, 2010 at 8:32 am
C:>ruby -v
ruby 1.9.1p378 (2010-01-10 revision 26273) [i386-mingw32]
C:>ruby -e “p String.included_modules”
[Comparable, Kernel]
I’ll try on 1.9.2 on a Linux box in a bit and post the results.
August 27, 2010 at 8:59 am
Got to know through RubyKoans that Ruby 1.8 and 1.9 differ in the way how single characters in strings are represented. Here is the excerpt from koans/about_strings.rb
====================================
in_ruby_version(“1.8″) do
def test_in_ruby_1_8_single_characters_are_represented_by_integers
assert_equal __, ?a
assert_equal __, ?a == 97
assert_equal __, ?b == (?a + 1)
end
end
in_ruby_version(“1.9″) do
def test_in_ruby_1_9_single_characters_are_represented_by_strings
assert_equal , ?a
assert_equal __, ?a == 97
end
end
====================================
Found this page with differences between 1.8 and 1.9:
http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9#l112
String: No longer an Enumerable
String is not Enumerable anymore. Use #each_line instead of #each, and #lines (see below) to iterate over the lines.
August 27, 2010 at 9:54 am
Not sure what was up this morning with my install of 1.9.2-p0 but now I get the following:
ruby-1.9.2-p0 > ‘this thing’.each{|a| puts a}‘
NoMethodError: undefined method `each’ for “this thing”:String
from (irb):1
from /Users/pivotal/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `
ruby-1.9.2-p0 > ‘this’.is_a? Enumerable
=> false
Maybe my machine was already checked out for the weekend already.
Final verdict is that in 1.8.x, each() on a String breaks on a line and in 1.9.x, String is no longer Enumerable so it is no longer an issue.
Thanks all for the comments and for keeping tabs on Pivotal Blabs.
August 27, 2010 at 10:03 am
The code from RubyKoans – with MarkDown
in_ruby_version(“1.8″) do
def test_in_ruby_1_8_single_characters_are_represented_by_integers
assert_equal 97, ?a
assert_equal true, ?a == 97
assert_equal true, ?b == (?a + 1)
end
end
in_ruby_version(“1.9″) do
def test_in_ruby_1_9_single_characters_are_represented_by_strings
assert_equal a, ?a
assert_equal __, ?a == 97
end
end
August 27, 2010 at 10:04 am
Matthew Closson, I liked your post but I wanted to give it a little poke.
You can drop the `m if `from your statement and write `String.instance_methods.each.select {|m| m.to_s.include?(‘each’)}`
`Array#select` returns an array of elements where the block evaluates to true :)
August 27, 2010 at 4:19 pm
@Matthew Closson, pmacek:
Even shorter…
> String.instance_methods.grep /each/
=> [:each_line, :each_byte, :each_char, :each_codepoint]
August 28, 2010 at 12:46 am
pmacek & pete — nice calls on both of those statement improvements.
Mike — glad to see that its figured out now, and look forward to future blog posts.
August 29, 2010 at 11:34 am