I rediscovered a nice way to give Ruby methods a more first-class feel when wanting to pass them around them without having to worry about knowledge of where the methods are defined. Lots of languages have this concept. Take JavaScript for example:

myFunc = function addTwo(arg) {
  return (arg + 2);
}

typeof myFunc;
//=> 'function'

myFunc(5);
//=> 7

In this way, we could pass a function to someone that wanted to use it to operate on something else

function caller(arg, func) {
  func.call(this, arg)
}

caller(2, myFunc)
//=> 4

Most Ruby code I’ve seen doesn’t use this same mechanism of passing functions to other functions. We, as superior beings to JS developers, have blocks after all.

def caller(arg, &block)
  block.call arg
end

caller do |arg|
  puts (arg + 2)
end

#=> 2

But what if we want to inject behavior into a caller from another class or use the same code for different blocks? Typically this involves needing to send across an object and a symbol representing the method to call via send or wrapping things in lambdas or Procs. Let’s set up an example:

class ZipUploader
  def initialize(zip)
    @zip = zip
  end

  def upload
    Unzipper.open(@zip).each_file do |file|
      TheCloud.upload file
    end
  end
end

Obviously, you don’t need to care how Unzipper or TheCloud work. Unzipper opens the zip files and allows for iterating over each file in the archive. TheCloud.upload uploads them to your favorite cloud storage provider.

Now imagine you want to do some additional processing for specific files in this archive but don’t want to pollute the uploader with knowledge outside of its main responsibility of getting files to the cloud. We could do this a few different ways:

class ZipUploader
  def initialize(zip)
    @zip = zip
    @callbacks = {}
  end

  def upload
    Unzipper.open(@zip).each_file do |file|
      run_callbacks(file)

      TheCloud.upload file.read
    end
  end

  # Option 1: pass a block
  def file_callback(file, &block)
    @callbacks[file] = block
  end

  private

  def run_callbacks(file)
    callback = @callbacks[file.name]
    callback.call(file) if callback
  end

  # Option 2: Pass a class and a symbol to call via `send`
  def file_callback(file, processor, action)
    @callbacks[file] = { runner: processor, method: action }
  end

  def run_callbacks(file)
    callback_defn = @callbacks[file.name]
    callback_defn[:runner].send(callback[:method], file)
  end
end

class Runner
  def initialize
    @uploader = ZipUploader.new("my_archive.zip")
  end

  def run
    # Using option 1
    uploader.file_callback("my_face.jpg") do |image|
      image = ImageMagic.superheroize(image)
    end

    # Or option 2
    uploader.file_callback("my_face.jpg", self.class, :superize)

    uploader.upload
  end

  def superize(image)
    image = ImageMagic.superheroize(image)
  end
end

So now, using either of the two options, as the uploader is uploading files, whenever it encounters a file called “my_face.jpg”, it will make it look like I’m a superhero. Awesome!

Obviously, option 2 is more complex and not as readable. The main reason we might do something like this when dealing with a system like Resque or Sidekiq, where you need jobs to be able to serialize callback info into Redis. You’d be better off storing class/method pairs over trying to serialize actual Proc or block calls.

So back to our example, in a real world application, we might have much more complex processing or a whole bunch of processors:

uploader.file_callback("*_avatar.jpg") do |image|
  image = ImageMagic.resize(image, width: 80, height: 80)
end

uploader.file_callback("*_retina.tif") do |image|
  image = ImageMagic.enhance(image)
end

# and so on...

That adds a lot of block code to wherever we’re setting up the uploader callbacks. What if we could just pass functions to our callback definitions? We could define these all as lamdas or Procs:

RESIZER = -> (img) { img = ImageMagic.resize(img, width: 80, height: 80) }
RETINIZER = Proc.new {|img| image = ImageMagic.enhance(image) }

uploader.file_callback("*_avatar.jpg", RESIZER)
uploader.file_callback("*_retina.tif", RETINIZER)

But that feels kind of ugly and un-Ruby. Well with method you can actually extract methods from a class and pass them to a function.

def resize(image)
  image = ImageMagic.resize(image, width: 80, height: 80)
end

def retinize(image)
  image = ImageMagic.enhance(image)
end

uploader.file_callback "*_avatar.jpg", &method(:resize)
uploader.file_callback "*_retina.tif", &method(:retinize)

The cool thing about this is you can even extract your image processing methods to a class of some kind, responsible for different processing types and still extract the functions and pass them to a call requiring a block.

class ImageHandlers
  def resize(image)
    image = ImageMagic.resize(image, width: 80, height: 80)
  end

  def retinize(image)
    image = ImageMagic.enhance(image)
  end
end

uploader.file_callback "*_avatar.jpg", &ImageHandlers.instance_method(:resize)
uploader.file_callback "*_retina.tif", &ImageHandlers.instance_method(:retinize)

What this method… uh… method returns is an instance of Ruby’s Method class. This class has a number of useful methods that you can use to build out much more functional-esque code. instance_method does the same thing for a class’s instance methods, only it returns an UnboundMethod instance, which is mostly the same thing, but with the added abilities to check things like super_method references and rebind to a different context or class/module.

If we wanted to use these Method objects to serialize to Redis, we could kinda fake it in the callback definition method, instead of storing the actual Method instance, we could serialize it using info we have available:

# callback is the Method we extract
def file_callback(file, callback)
  # holds the callback reference in a string: "Class#method"
  @callbacks[file] = "#{callback.owner}##{callback.original_name}"
end

def run_callbacks(file)
  callback_defn = @callbacks[file.name]
  klass, meth = callback_defn.split('#')

  callback = klass.constantize.instance_method(meth)
  callback.send(file)

  # or just
  klass.constantize.send(meth, file)
end

This would allow you to serialize callbacks if need be. This will still allow you to pass methods as parameters in the calling code, allowing you to simply define methods on your class and pass them to the processor.

Subnotes

Like subtweeting, but for blog posts

class Processor
  def plus_two(arg)
    arg + 2
  end
end

class Runner
  def block_run(arg, &block)
    yield arg
  end

  def sender(arg, cls, mth)
    cls.send mth, arg
  end
end

lambda = -> (arg) { arg + 2 }
proc = Proc.new {|arg| arg + 2 }

processor = Processor.new
runner = Runner.new

meth = processor.method(:plus_two)
rebinder = Processor.new
rebound = meth.unbind.bind rebinder

Benchmark.ips do |test|
  test.report("Direct calls") { processor.plus_two(2) }
  test.report("Via send") { runner.sender(2, processor, :plus_two) }
  test.report("Blocks") { runner.block_run(2) {|arg| arg + 2 } }
  test.report("Lambda") { runner.block_run(2, &lambda) }
  test.report("Proc") { runner.block_run(2, &proc) }
  test.report("Method") { runner.block_run(2, &meth) }
  test.report("Rebound direct call") { rebinder.plus_two(2) }
  test.report("Rebound method") { runner.block_run(2, &rebound) }

  test.compare!
end
Calculating -------------------------------------
        Direct calls   135.058k i/100ms
            Via send   125.582k i/100ms
              Blocks    79.429k i/100ms
              Lambda   129.758k i/100ms
                Proc   122.336k i/100ms
              Method    60.091k i/100ms
 Rebound direct call   139.934k i/100ms
      Rebound method    59.337k i/100ms
-------------------------------------------------
        Direct calls      7.723M (± 6.9%) i/s -     38.492M
            Via send      5.190M (± 6.6%) i/s -     25.870M
              Blocks      1.515M (± 7.4%) i/s -      7.546M
              Lambda      5.252M (± 7.2%) i/s -     26.211M
                Proc      5.446M (± 5.3%) i/s -     27.159M
              Method    998.451k (± 6.2%) i/s -      4.988M
 Rebound direct call      8.097M (± 5.7%) i/s -     40.441M
      Rebound method      1.018M (± 5.7%) i/s -      5.103M

Comparison:
 Rebound direct call:  8096886.8 i/s
        Direct calls:  7722969.7 i/s - 1.05x slower
                Proc:  5445994.0 i/s - 1.49x slower
              Lambda:  5251876.4 i/s - 1.54x slower
            Via send:  5190066.6 i/s - 1.56x slower
              Blocks:  1515285.7 i/s - 5.34x slower
      Rebound method:  1017916.6 i/s - 7.95x slower
              Method:   998451.0 i/s - 8.11x slower

So obviously there are some performance concerns with a number of these approaches. I’m unsure as to why, other than possibly internal Ruby VM optimizations for regular method calls. Surprisingly, a block is actually slower than passing procs/lambdas or using send, though probably for the same reasons that using Method instances are slower too. Maybe I’ll dig on that in my next post!