How do you stringize/serialize Ruby code?
Asked Answered
A

4

17

I want to be able to write a lambda/Proc in my Ruby code, serialize it so that I can write it to disk, and then execute the lambda later. Sort of like...

x = 40
f = lambda { |y| x + y }
save_for_later(f)

Later, in a separate run of the Ruby interpreter, I want to be able to say...

f = load_from_before
z = f.call(2)
z.should == 42

Marshal.dump does not work for Procs. I know Perl has Data::Dump::Streamer, and in Lisp this is trivial. But is there a way to do it in Ruby? In other words, what would be the implementation of save_for_later?

Edit: My answer below is nice, but it does not close over free variables (like x) and serialize them along with the lambda. So in my example ...

x = 40
s = save_for_later { |y| x + y }
# => "lambda { |y|\n  (x + y)\n}"

... the string output does not include a definition for x. Is there a solution that takes this into account, perhaps by serializing the symbol table? Can you access that in Ruby?

Edit 2: I updated my answer to incorporate serializing local variables. This seems acceptable.

Anastase answered 14/10, 2008 at 0:44 Comment(1)
Why am I getting -1'd on this question? I didn't answer or comment hereTrochal
A
13

Use Ruby2Ruby

def save_for_later(&block)
  return nil unless block_given?

  c = Class.new
  c.class_eval do
    define_method :serializable, &block
  end
  s = Ruby2Ruby.translate(c, :serializable)
  s.sub(/^def \S+\(([^\)]*)\)/, 'lambda { |\1|').sub(/end$/, '}')
end

x = 40
s = save_for_later { |y| x + y }
# => "lambda { |y|\n  (x + y)\n}"
g = eval(s)
# => #<Proc:0x4037bb2c@(eval):1>
g.call(2) 
# => 42

This is great, but it does not close over free variables (like x) and serialize them along with the lambda.

To serialize variables also, you can iterate over local_variables and serialize them as well. The problem, though, is that local_variables from within save_for_later accesses only c and s in the code above -- i.e. variables local to the serialization code, not the caller. So unfortunately, we must push the grabbing of local variables and their values to the caller.

Maybe this is a good thing, though, because in general, finding all free variables in a piece of Ruby code is undecidable. Plus, ideally we would also save global_variables and any loaded classes and their overridden methods. This seems impractical.

Using this simple approach, you get the following:

def save_for_later(local_vars, &block)
  return nil unless block_given?

  c = Class.new
  c.class_eval do
    define_method :serializable, &block
  end
  s = Ruby2Ruby.translate(c, :serializable)
  locals = local_vars.map { |var,val| "#{var} = #{val.inspect}; " }.join
  s.sub(/^def \S+\(([^\)]*)\)/, 'lambda { |\1| ' + locals).sub(/end$/, '}')
end

x = 40
s = save_for_later(local_variables.map{ |v| [v,eval(v)] }) { |y| x + y }
# => "lambda { |y| _ = 40; x = 40;\n  (x + y)\n}"

# In a separate run of Ruby, where x is not defined...
g = eval("lambda { |y| _ = 40; x = 40;\n  (x + y)\n}")
# => #<Proc:0xb7cfe9c0@(eval):1>
g.call(2)
# => 42

# Changing x does not affect it.
x = 7
g.call(3)
# => 43
Anastase answered 14/10, 2008 at 2:16 Comment(3)
My version of Ruby2Ruby (1.2.4) doesn't seem to have a translate method. Is there a different api for this in newer versions?Nephelometer
I don't quite understand it, but this thread recommends downgrading to Ruby2Ruby 1.2.2. I also found this monkey patch which claims to add it back to later versions. Haven't tried it though.Anastase
@Kludge You can use sourcify instead. See my other answer.Anastase
A
11

Use sourcify

This will work on Ruby 1.8 or 1.9.

def save_for_later(&block)
  block.to_source
end

x = 40
s = save_for_later {|y| x + y }
# => "proc { |y| (x + y) }"
g = eval(s)
# => #<Proc:0x00000100e88450@(eval):1>
g.call(2) 
# => 42

See my other answer for capturing free variables.

Update: Now you can also use the serializable_proc gem, which uses sourcify, and captures local, instance, class, and global variables.

Anastase answered 14/10, 2008 at 0:44 Comment(3)
Is there an example anywhere of using sourcify with capturing free variables?Recrudescence
@rogerdpack, see my update. You may just want to use the serializable_proc gem if you care about free variables.Anastase
sourcify/ SerializableProc: no longer maintained (at least today)Infinity
C
2

Check out the answers to this question.

Cecilla answered 14/10, 2008 at 1:40 Comment(0)
L
-9

Ruby has the Marshal class that has a dump method that you can call.

Take a look here:

http://rubylearning.com/satishtalim/object_serialization.html

Litigation answered 14/10, 2008 at 0:49 Comment(1)
Marshal doesn't actually work on Procs, which what the question is asking about.Cinerator

© 2022 - 2024 — McMap. All rights reserved.