What are the Ruby Gotchas a newbie should be warned about? [closed]

I have recently learned the Ruby programming language, and all in all it is a good language. But I was quite surprised to see that it was not as simple as I had expected. More precisely, the "rule of least-surprise" did not seem very respected to me (of course this is quite subjective). For example:

x = true and false
puts x  # displays true!

and the famous:

puts "zero is true!" if 0  # zero is true!

What are the other "Gotchas" you would warn a Ruby newbie about?


Solution 1:

Wikipedia Ruby gotchas

From the article:

  • Names which begin with a capital letter are treated as constants, so local variables should begin with a lowercase letter.
  • The characters $ and @ do not indicate variable data type as in Perl, but rather function as scope resolution operators.
  • To denote floating point numbers, one must follow with a zero digit (99.0) or an explicit conversion (99.to_f). It is insufficient to append a dot (99.), because numbers are susceptible to method syntax.
  • Boolean evaluation of non-boolean data is strict: 0, "" and [] are all evaluated to true. In C, the expression 0 ? 1 : 0 evaluates to 0 (i.e. false). In Ruby, however, it yields 1, as all numbers evaluate to true; only nil and false evaluate to false. A corollary to this rule is that Ruby methods by convention — for example, regular-expression searches — return numbers, strings, lists, or other non-false values on success, but nil on failure (e.g., mismatch). This convention is also used in Smalltalk, where only the special objects true and false can be used in a boolean expression.
  • Versions prior to 1.9 lack a character data type (compare to C, which provides type char for characters). This may cause surprises when slicing strings: "abc"[0] yields 97 (an integer, representing the ASCII code of the first character in the string); to obtain "a" use "abc"[0,1] (a substring of length 1) or "abc"[0].chr.
  • The notation statement until expression, unlike other languages' equivalent statements (e.g. do { statement } while (not(expression)); in C/C++/...), actually never runs the statement if the expression is already true. This is because statement until expression is actually syntactic sugar over

    until expression
      statement
    end
    

    , the equivalent of which in C/C++ is while (not(expression)) statement; just like statement if expression is an equivalent to

    if expression
      statement
    end
    

    However, the notation

    begin
      statement
    end until expression
    

    in Ruby will in fact run the statement once even if the expression is already true.

  • Because constants are references to objects, changing what a constant refers to generates a warning, but modifying the object itself does not. For example, Greeting << " world!" if Greeting == "Hello" does not generate an error or warning. This is similar to final variables in Java, but Ruby does also have the functionality to "freeze" an object, unlike Java.

Some features which differ notably from other languages:

  • The usual operators for conditional expressions, and and or, do not follow the normal rules of precedence: and does not bind tighter than or. Ruby also has expression operators || and && which work as expected.

  • def inside def doesn't do what a Python programmer might expect:

    def a_method
        x = 7
        def print_x; puts x end
        print_x
    end
    

    This gives an error about x not being defined. You need to use a Proc.

Language features

  • Omission of parentheses around method arguments may lead to unexpected results if the methods take multiple parameters. The Ruby developers have stated that omission of parentheses on multi-parameter methods may be disallowed in future Ruby versions; the current (November 2007) Ruby interpreter throws a warning which encourages the writer not to omit (), to avoid ambiguous meaning of code. Not using () is still common practice, and can be especially nice to use Ruby as a human readable domain-specific programming language itself, along with the method called method_missing().

Solution 2:

Newbies will have trouble with equality methods:

  • a == b : checks whether a and b are equal. This is the most useful.
  • a.eql? b : also checks whether a and b are equal, but it is sometimes more strict (it might check that a and b have the same type, for example). It is mainly used in Hashes.
  • a.equal? b : checks whether a and b are the same object (identity check).
  • a === b : used in case statements (I read it as "a matches b").

These examples should clarify the first 3 methods:

a = b = "joe"

a==b       # true
a.eql? b   # true
a.equal? b # true (a.object_id == b.object_id)

a = "joe"
b = "joe"

a==b       # true
a.eql? b   # true
a.equal? b # false (a.object_id != b.object_id)

a = 1
b = 1.0

a==b       # true
a.eql? b   # false (a.class != b.class)
a.equal? b # false

Note that ==, eql? and equal? should always be symmetrical : if a==b then b==a.

Also note that == and eql? are both implemented in class Object as aliases to equal?, so if you create a new class and want == and eql? to mean something else than plain identity, then you need to override them both. For example:

class Person
    attr_reader name
    def == (rhs)
      rhs.name == self.name  # compare person by their name
    end
    def eql? (rhs)
      self == rhs
    end
    # never override the equal? method!
end

The === method behaves differently. First of all it is not symmetrical (a===b does not imply that b===a). As I said, you can read a===b as "a matches b". Here are a few examples:

# === is usually simply an alias for ==
"joe" === "joe"  # true
"joe" === "bob"  # false

# but ranges match any value they include
(1..10) === 5        # true
(1..10) === 19       # false
(1..10) === (1..10)  # false (the range does not include itself)

# arrays just match equal arrays, but they do not match included values!
[1,2,3] === [1,2,3] # true
[1,2,3] === 2       # false

# classes match their instances and instances of derived classes
String === "joe"   # true
String === 1.5     # false (1.5 is not a String)
String === String  # false (the String class is not itself a String)

The case statement is based on the === method:

case a
  when "joe": puts "1"
  when 1.0  : puts "2"
  when (1..10), (15..20): puts "3"
  else puts "4"
end

is equivalent to this:

if "joe" === a
  puts "1"
elsif 1.0 === a
  puts "2"
elsif (1..10) === a || (15..20) === a
  puts "3"
else
  puts "4"
end

If you define a new class whose instances represent some sort of container or range (if it has something like an include? or a match? method), then you might find it useful to override the === method like this:

class Subnet
  [...]
  def include? (ip_address_or_subnet)
    [...]
  end
  def === (rhs)
    self.include? rhs
  end
end

case destination_ip
  when white_listed_subnet: puts "the ip belongs to the white-listed subnet"
  when black_listed_subnet: puts "the ip belongs to the black-listed subnet"
  [...]
end

Solution 3:

  • Monkey patching. Ruby has open classes, so their behaviour can be dynamically changed at runtime...

  • Objects might respond to undefined methods if method_missing or send has been overridden. This exploits Ruby's message-based method invocation. Rails' ActiveRecord system uses this to great effect.

Solution 4:

The following code surprised me. I think it's a dangerous gotcha: both easy to run into, and hard to debug.

(1..5).each do |number|
  comment = " is even" if number%2==0
  puts number.to_s + comment.to_s
end

This prints:

1
2 is even
3
4 is even
5

But if I just add comment =anything before the block...

comment = nil
(1..5).each do |number|
  comment = " is even" if number%2==0
  puts number.to_s + comment.to_s
end

Then I get:

1
2 is even
3 is even
4 is even
5 is even

Basically, when a variable is only defined inside a block, then it is destroyed at the end of the block, and then it gets reset to nil upon every iteration. That's usually what you expect. But if the variable is defined before the block, then the outer variable is used inside the block, and its value is therefore persistent between iterations.

One solution would be to write this instead:

comment = number%2==0 ? " is even" : nil

I think a lot of people (including me) tend to write "a = b if c" instead of "a = (c ? b : nil)", because it's more readable, but obviously it has side-effects.