Programming ≈ Fun

Written by Krešimir Bojčić

Blow Your Mind (Maybe) With Case Equality in Ruby

It all starts with “A Little Unnecessary Smalltalk Envy” from Bob Hutchinson.

Quick warning: If you don’t like monkey patching… Run away now it is still not too late…

# Copied from
# https://blog.teksol.info/2007/11/23/a-little-smalltalk-in-ruby-if_nil-and-if_not_nil
# https://recursive.ca/hutch/2007/11/22/a-little-unnecessary-smalltalk-envy/
# Bob Huntchison
# Shortened to just support if_not_nil
class Object
  def if_not_nil(&block) yield(self) if block end
end

class NilClass
  def if_not_nil(&block) end
end

This enables you to stop treating nil as a special case:

class Person
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

def person_that_exists
  Person.new("drKreso")
end

def non_existing_person
  nil
end

person_that_exists.if_not_nil { |person| puts "found #{person.name}"} # => found drKreso
non_existing_person.if_not_nil { |person| puts "found #{person.name}"} # => nothing happens

This is easy enough to comprehend. We enabled NilClass to respond to if_not_nil by doing nothing. Normal object reacts on the same message by injecting self to the block and having wanted side effects. Is it useful? I don’t know but I think it’s cute.

Yesterday I saw this taken a step further in the example that is (I think) created for the new book Objects on Rails whose beta I’ve read and highly recommend. Original is available at Avdi Grimm gist example.

The cool part is this short but sweet addition to Object (I told you about monkey patching right?)

class Object
  def when(matcher)
    if matcher === self then yield(self) else self end
  end
end

Now you don’t have special cases for if_nil or if_not_nil. Actually you can stick in any lambda you want (or so I thought). When the condition is satisfied you get to execute the block with self injected in it. Otherwise you get a no go!

person_that_exists.when(->(p){ !p.nil? }) { |p| puts "found #{p.name}"} #=> found drKreso
non_existing_person.when(nil) { puts "Nothing, move along"} #=> Nothing, move along

And now here is the part(finally) that blew my mind. Since you need to put in Proc and Proc#=== is defined as : “Invokes the block with obj as the proc’s parameter like Proc#call.”
This question arises: How come I am not able to rewrite the when definition with Proc#call? It just doesn’t add up… This was the most explicit version that I could write that (I speculated) does the same thing:

def when(matcher, &block)
  if (matcher.nil? || matcher.call(self)) then block.call(self) else self end
end

I read it like this: If matcher is nil or if calling the matcher lambda/proc gives me true then inject self into the block. Otherwise just pass self. Now this was problematic because I had to deal with special case when matcher was nil. Otherwise I could not get it to work… yet #=== worked just fine.

The answer turned out pretty simple (Thanks @avdi)

It does not have to be lambda(proc) it can be anything that has #=== defined and that would be well anything derived from Object and that would be: everything. (I am not going to dwell about BasicObject here.) So it turns out that Proc happens to invoke call for #=== which is rather fortunate.OH: now it all makes sense, and I fell a bit well you know…

Since NilClass does not have a #=== defined and since it is derived from Object it ends up calling Object#=== that turns to call Object#==. Inheritance in its full glory, you get to use anything you want for a matcher. End of story.

Conclusion

If you ignore my unnecessary wanderings this is pretty neat technique. I am sorry I skimmed the sections about Case Equality (===) in all the books I’ve read since it turned out pretty useful. That with combination of me obsessing that it has to be lambda blew my mind pretty well so I thought this might help someone.

« Refactoring My Basement Quality Is Overrated »

Copyright © 2019 - Kresimir Bojcic (LinkedIn Profile)