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
Written by Jesper Josefsson
Related protips
Have a fresh tip? Share with Coderwall community!
Post
Post a tip
Best
#Ruby
Authors
Sponsored by #native_company# — Learn More
#native_title#
#native_desc#