Skip to content

Instantly share code, notes, and snippets.

@rkh
Created April 21, 2009 23:06
Show Gist options
  • Save rkh/99457 to your computer and use it in GitHub Desktop.
Save rkh/99457 to your computer and use it in GitHub Desktop.
require "cheap_proc"
file = "/tmp/stored_proc"
unless File.exists? file
foo = 42
cheap = lambda { puts foo }.to_cheap_proc
File.open(file, "w") { |f| f.write(cheap.dump) }
else
cheap = File.read(file).to_cheap_proc
end
cheap.call
#!/usr/bin/env ruby
#
# This library contains an alternative closure implementation that is both more lightweight
# than ruby's build-in Proc class and can be stored in a string (read: a file, can be sent
# over network, etc.)
require "rubygems"
require "sexp_processor"
require "parse_tree"
require "ruby2ruby"
TOPLEVEL_OWNER = self
# A CheapProc is a Proc replacement using less memory than Proc. Besides this, it can be
# stored in a string (and of course can be restored from a string) and still keep the original
# scope. This is accomplished by a pure-ruby upval implementation (simmilar to what tinyrb/lua does).
class CheapProc
class BlockParser < SexpProcessor # :nodoc:
attr_accessor :scope
def lvars
@lvars ||= eval("local_variables", scope)
end
def upvals
@upvals ||= {}
end
def process_call(sexp)
upvals[sexp[1]] = eval(sexp[1], scope) if sexp.first.nil? and lvars.include? sexp[1]
result = sexp.dup
sexp.clear
result
end
end
def self.from_proc(block)
new.tap do |cheap|
scope = block.binding
owner = eval("self", scope)
unifier = Unifier.new.tap { |u| u.processors.each { |p| p.unsupported.delete :cfunc } }
parser = BlockParser.new
parser.scope = scope
cheap.block = block
cheap.sexp = parser.process unifier.process(ParseTree.new.parse_tree_for_proc(block))
cheap.upvals = parser.upvals
cheap.block_owner = owner unless owner == TOPLEVEL_OWNER
cheap.freeze
end
end
def self.from_string(string)
new.tap do |cheap|
Marshal.load(string).each { |k, v| cheap.send("#{k}=", v) }
cheap.freeze
end
end
attr_accessor :sexp, :upvals, :block, :block_owner
# for really using less space than a proc, one has to call this after creating a cheap proc form a proc
def clean_up
@block, @fixed_sexp = nil, nil
GC.start
end
def frozen?
!!@frozen
end
def freeze
@frozen = true
upvals.freeze
self.sexp = sexp.to_a.freeze
class << self
%w(sexp upvals block block_owner).each do |attr|
define_method("#{attr}=") { raise TypeError, "can't modify frozen CheapProc" }
end
end
self
end
def to_cheap_proc
self
end
def dump
Marshal.dump "upvals" => upvals, "block_owner" => block_owner, "sexp" => sexp
end
def to_ruby
Ruby2Ruby.new.process to_sexp
end
def to_sexp
@fixed_sexp ||= sexp.dup.tap do |fixed|
fixed[3] = upvals.inject(s(:block)) { |sx, (k, v)| sx << s(:lasgn, k.to_sym, s(:lit, v)) } << fixed[3]
end
end
def to_proc
@block ||= block_owner ? block_owner.instance_eval(to_ruby) : eval(to_ruby, TOPLEVEL_BINDING)
end
def method_missing(name, *a, &b)
return super unless Proc.instance_methods.include? name.to_s
to_proc.send(name, *a, &b)
end
end
class Object
unless respond_to? :tap
def tap
yield(self)
self
end
end
end
class Proc
def self.===(other)
super or CheapProc === other
end
def to_cheap_proc
CheapProc.from_proc(self)
end
end
class String
def to_cheap_proc
CheapProc.from_string(self)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment