Problem

I’ve recently noticed I often use code like that.

  class Offer
    def available?
    end

    def cancelled?
    end

    def postponed?
    end
  end

  if offer.available?
    order.submit
  elsif offer.cancelled?
    order.reject
  elsif offer.postponed?
    order.postpone
  else raise UnknownStateError.new
  end

if/else statements looks like perfect candidates for case … when statement. In particular I’d like to have something like that:

  case offer
  when :available? then order.submit
  when :cancelled? then order.reject
  when :postponed? then order.postpone
  else raise UnknownStateError.new

Looks better, right? Of course it doesn’t work out of the box. We’ll try to overcome it but first some theory.

case/when uses === method internally. In particular ruby calls code like this:

  :available?.===(offer)

…which will always return false when compared to non-symbol. Triple equals (===) is slightly different that double equals (==) so be awared when each of them is called.

Solution

When we know what is going under the hood it is easy to slightly modify the behaviour to fit our needs.

class Symbol
  alias :original_triple_equals :"==="

  def ===(object)
    original_triple_equals(object) ||
      (object.respond_to?(self) && object.__send__(self))
  end

end

We override triple equals and try to call a desired method if original behaviour returns false. Let’s test it

case []
when :empty? then puts "that works!"
else puts "something went wrong"

Works like a charm.

Performance

There is no doubt performance will be affected. Let’s benchmark performance of comparing symbols.

REPEAT = 1_000_000
Benchmark.bm do |x|
  x.report("original  ===") do
    REPEAT.times { :s1.original_triple_equals(:s2) }
  end
  x.report("overriden ===") do
    REPEAT.times { :s1.===(:s2) }
  end
end

Ruby 1.8:

                  user     system      total        real
original  ===  0.610000   0.170000   0.780000 (  0.782712)
overriden ===  1.530000   0.330000   1.860000 (  1.852536)

Performance hit: 238%

Ruby 1.9:

                  user     system      total        real
original  ===  0.180000   0.000000   0.180000 (  0.187665)
overriden ===  0.580000   0.000000   0.580000 (  0.576526)

Performance hit: 322%

Benchmark isn’t the most accurate but let’s approximate that decrease in performance is 200%-400%. Is it much? It depends how often you symbols are compared but under some circumstances it may lead for serious performance problems. I warned you.

Ruby 1.9

Ruby 1.9 comes with a new method of calling blocks.

  proc { puts "it looks awkward" }.===

It gives us some cool capabilities. Take a look at that example.

How does it help us calling methods in case … when statement? Frankly… not much :) But we can do some interesting tricks for fun and profit.

case []
when :empty?.to_proc then puts "empty array"
else puts "Ruby 1.9?"
end

We convert Symbol to proc which was introduced in Ruby 1.8.7 and then case … when calls proc.=== implicity. Of course calling to_proc each time isn’t very helpful and readability is even worse.

Quite better solution which comes to my mind is to introduce a method which takes a symbol and call to_proc on it. The method should be globally accessible so we have to define it at kernel level.

module Kernel
  def is(symbol)
    symbol.to_proc
  end
end

case []
when is(:empty?) then puts "empty array"
else puts "Ruby 1.9?"
end

Unfortunately we can’t skip parentheses here.

There is a big benefit here. We don’t override symbol.=== so performance remains unaffected. On the other hand polluting Kernel namespace is a certain drawback.

Of course Ruby 1.9 solution may be implemented in Ruby 1.8 as well. You would just need to implement Proc#=== method.

Update: Jacob’s Solution

Jacob Rothstein introduced very neat solution using hashes and lambdas. It doesn’t use case … when statement at all but behavior is very similar.

class Object
  def switch( hash )
    hash.each {|method, proc| return proc[] if send method }
    yield if block_given?
  end
end

# which allows us to write functional-like code

offer.switch(
  available: -> { order.submit },
  cancelled: -> { order.reject },
  postponed: -> { order.postpone }
) { raise UnknownStateError.new }

It depends on new lambda syntax and ordered hashes so Ruby 1.9 is required. This is very clear and doesn’t pollute Symbol#=== method. Read full explanation here.

WrocLove.rb