Programming ≈ Fun

Written by Krešimir Bojčić

Faking Multiple Blocks in Ruby

Three months ago I was comparing Lisp, Smalltalk and Ruby. I was struggling to create new language syntax (for example my version of if..else) that is so easy to do in Lisp, and also possible in Smalltalk:

In Smalltalk it would be used like this:

(1 = 1)
    ifCorrect:    [Transcript show: 'I am correct']
    ifNotCorrect: [Transcript show: 'I am wrong'].
My knowledge of Smalltalk is non-existing, but look at the beautiful [] block syntax and notice the keyword arguments being able to accept multiple blocks.

Implementation in Smalltalk is both straightforward and cool:

# this is in True class
ifCorrect: trueAlternativeBlock ifNotCorrect: falseAlternativeBlock
     ^trueAlternativeBlock value

# this is in False class
ifCorrect: trueAlternativeBlock ifNotCorrect: falseAlternativeBlock
     ^falseAlternativeBlock value

In Ruby I was missing the ability to have multiple blocks in method call that would enable me to evaluate only one leg. This was best I could come up with and it included (mis)using the lambdas:

class FalseClass
  def if_correct(trueBlock, hash)
    hash[:if_not_correct].call
  end
end

class TrueClass
  def if_correct(trueBlock, hash)
    trueBlock.call
  end
end

(1==0).if_correct ->{ print "Hooray it's true"},
       if_not_correct:->{print "Not true"}

Today I’ve read the excellent post about using Ruby Blocks as Dynamic Callbacks. I was mesmerized, but of course I didn’t get it. Than I saw really kick ass explanation here. Holding hands did have effect even on me. Still I am not sure that I would be able to pull it of on my own without watching the original code. At the end I did saw one gist in comments claiming to have similar pattern. I don’t know if it’s better, but it looks less intrusive because there is no patching of Proc class and it clicked more easily.

As an exercise I’ve decided to use technique described there to implement my version of if..else:

#1
Custom.if(1 == 0) do |condition|
  condition.true? do
    puts "True"
  end.false? do
    puts "False"
  end
end

#2
Custom.if(1 == 0) do |condition|
  condition.true? { puts "True" }.
  false? { puts "False" }
end

#3 (my favourite)
Custom.if(1 == 0) do |condition|
  condition.true? { puts "True" }
  condition.false? { puts "False" }
end
...

It’s relatively easy to explain how it works. Look at the definition of Custom class:

class Custom
  def self.if(condition, &block)
    proxy = BlockProxy[block]
    if condition
      proxy.call_block_for :true?
    else
      proxy.call_block_for :false?
    end
  end

  class BlockProxy
    def self.[](block)
      new.tap { |proxy| block.call(proxy) }
    end

    def call_block_for(callback, *args)
      callbacks[callback].call(*args)
    end

    def method_missing(m, *args, &block)
      block ? callbacks[m] = block : super
      self
    end

    def callbacks
      @callbacks ||= {}
    end
  end
end

line 2 : Using self.if method that receives condition and the block

codition ==> (1 == 0) ==> false
block  ==>
  condition.true? do
    puts "True"
  end.false? do
    puts "False"
  end
  • line 3 : Creating the BlockProxy that is responding to missing method call in a way to:
    • If there is passed in block then store it under method name in @callbacks variable
    • If there is no passed block then raise the classic no method found exception

If you now look at usage of Custom class

Custom.if(1 == 0) do |condition|
  condition.true? do
    puts "True"
  end.false? do
    puts "False"
  end
end

notice that |condition| is really a BlockProxy object that you’ve tapped in on line 13.
So when you call:

condition.true?

you invoke method_missing on a BlockProxy that stores received block in @callbacks hash with :true? key:

#stored block under :true? key
puts "True"

It’s all great from there on. Next call to false? invokes method_missing again and stores passed in block under :false? key:

#stored block under :false? key
puts "False"

When all of this is set and done I get to implement my version of if..else with built in if..else (line 4..8 in Custom class) :)

Conclusion

The thing is that with this technique I can do whatever I want with my version of if..else. I may alter it to log something when condition is true or do something similarly outrageous. Also it looks as if I could easily implement switch…case idiom etc. Is it practical to replace existing syntax? I don’t think so, but it really is intriguing and I am glad that it can be pulled of in Ruby like this. I want to thank Matt Sears, @bodhi and Ryan LeCompte for taking this one off of my chest.

This technique does give you yet another tool for creating pure Ruby DSL-s.

« Decorators in Ruby Refactoring My Basement »

Comments

Copyright © 2019 - Kresimir Bojcic (LinkedIn Profile)