1

I took a look on the Ruby on Rails source code and found code like this:

case options
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
  ...
when String
  ...
when :back
  ...
when Proc
  ...
end

where options can be a String, Symbol, Proc, or Hash object. The === comparison will return true only in one case:

'string' === /string/ # => false
:back === :back # => true
(Proc.new {}) === Proc # => nil
Hash.new === Hash # => false

How does Ruby case work, allowing match with such different cases?

sawa
  • 165,429
  • 45
  • 277
  • 381
Mihails Butorins
  • 918
  • 8
  • 14

2 Answers2

4

A wrong assumption that you (as well as many beginners) seem to make is that === is symmetric. It actually is not. x === y works differently not depending on y, but depending on x.

Different classes have different definitions for ===. The expression x === y is the same as:

  • y == x (for x: instance of String, Symbol, Hash, etc.)
  • y =~ x(for x: instance of Regexp)
  • y.kind_of?(x) (for x: instance of Class)

Also, you may be confusing a class and its instance. /regexp/ === is not the same as Regexp ===. "string" === is not the same as String ===, etc.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • 1
    The operand order can also be confusing. `case obj; when pattern; end` invokes `pattern === obj`, not `obj === pattern`. – Stefan Jan 09 '16 at 09:00
3

It might help to understand that:

"foo" === /foo/

is actually another way to write:

"foo".===(/foo/)

So it's actually a method call to the instance method String#=== on "foo" (which is an instance of class String), passing it /foo/ as the argument. Therefore what will happen is totally defined by String#===.

In your code, what actually happens is this:

if /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i === options
  # ...
elsif String === options
  # ...
elsif :back === options
  # ...
elsif Proc === options
  # ...
else
  # ...
end.delete("\0\r\n")

Therefore your case statements are actually method calls to (in order of appearance):

  1. Regex#===
  2. Module#===
  3. Symbol#===
  4. and then again Module#===

Regarding the second example in your question:

'string' === /string/ # => false

This above is calling String#=== which, according to the documentation:

Returns whether str == obj, similar to Object#==.

If obj is not an instance of String but responds to to_str, then the two strings are compared using case equality Object#===.

Otherwise, returns similarly to #eql?, comparing length and content.

That's why it doesn't match, because it actually calls Object#==.

Similarly:

# this calls `Proc#===`, not `Module#===`
(Proc.new {}) === Proc # => false

# however, if you want to test for class equality you should do:
Proc === (Proc.new {}) # => true, calls `Module#===`

Same goes for Hash.new === Hash.

Agis
  • 32,639
  • 3
  • 73
  • 81