Last Updated: February 25, 2016
·
389
· jesjos

Rebinding a proc

As I was writing a DSL-ish thing, where you would define database migrations connected to classes using a class method, I ran into the need to rebind a proc to a new context.

The class method converts the block into a Proc, wraps it in a Migration object and adds it to a class-level SortedSet.

The naive approach

This was my first naïve attempt:

class SimpleMigrator
  def migrate(name, proc)
    proc.call(:database)
  end
end

module Migratable
  def migrate!
    self.class.migrations.each do |migration|
      migrator.migrate(migration.name, migration.proc)
    end
  end

  def migrator
    @migrator ||= SimpleMigrator.new
  end

  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def migration(name, &block)
      migrations.add(Migration.new(name, block))
    end

    def migrations
      @migrations ||= SortedSet.new
    end
  end
end

class Foo
  include Migratable

  migration("20140929") do |db|
    my_method
  end

  def my_method
    "foo"
  end
end
Foo.migrations.first.proc
# => the block we gave to the migration-method

The problem is that the block is bound to the wrong context - in this case the class:

Foo.new.migrate!
# => undefined method `my_method' for Foo:Class

instance_eval and instance_exec

I first tried using instance_eval, but it doesn't allow for sending arguments to the proc.

One solution is to use instance_execute.

module Migratable

  def migrate!
    self.class.migrations.each do |migration|
      rebound_proc = rebind_proc(migration.proc)
      migrator.migrate(migration.name, rebound_proc)
    end
  end

  def rebind_proc(proc)
    Proc.new do |db|
      instance_exec(db, &proc)
    end
  end

  # rest of module omitted

end

Currying the context

Björn Skarner suggested another approach - currying the procs with the context. That could look something like this:

module Migratable

  def migrate!
    self.class.migrations.each do |migration|
      curried = migration.proc.curry[self]
      migrator.migrate(migration.name, curried)
    end
  end

  # code omitted

  module ClassMethods
    def migration(name, &block)
      migration_proc = Proc.new do |migratable, db|
        migratable.instance_exec(db, &block)
      end
      migrations.add(Migration.new(new, migration_block))
    end
  end
  # code omitted

end