Class: SerializableProc

Inherits:
Object
  • Object
show all
Includes:
Marshalable
Defined in:
lib/serializable_proc.rb,
lib/serializable_proc/binding.rb,
lib/serializable_proc/parsers.rb,
lib/serializable_proc/isolatable.rb,
lib/serializable_proc/parsers/pt.rb,
lib/serializable_proc/parsers/rp.rb,
lib/serializable_proc/marshalable.rb

Overview

SerializableProc differs from the vanilla Proc in 2 ways:

#1. Isolated variables

By default, upon initializing, all variables (local, instance, class & global) within its context are extracted from the proc’s binding, and are isolated from changes outside the proc’s scope, thus, achieving a snapshot effect.

require 'rubygems'
require 'serializable_proc'

x, @x, @@x, $x = 'lx', 'ix', 'cx', 'gx'

s_proc = SerializableProc.new { [x, @x, @@x, $x].join(', ') }
v_proc = Proc.new { [x, @x, @@x, $x].join(', ') }

x, @x, @@x, $x = 'ly', 'iy', 'cy', 'gy'

s_proc.call # >> "lx, ix, cx, gx"
v_proc.call # >> "ly, iy, cy, gy"

It is possible to fine-tune how variables isolation is being applied by declaring @@_not_isolated_vars within the code block:

x, @x, @@x, $x = 'lx', 'ix', 'cx', 'gx'

s_proc = SerializableProc.new do
  @@_not_isolated_vars = :global, :class, :instance, :local
  [x, @x, @@x, $x].join(', ')
end

x, @x, @@x, $x = 'ly', 'iy', 'cy', 'gy'

# Passing Kernel.binding is required to avoid nasty surprises
s_proc.call(binding) # >> "ly, iy, cy, gy"

Note that it is strongly-advised to append Kernel.binding as the last parameter when invoking the proc to avoid unnecessary nasty surprises.

#2. Marshallable

No throwing of TypeError when marshalling a SerializableProc:

Marshal.load(Marshal.dump(s_proc)).call # >> "lx, ix, cx, gx"
Marshal.load(Marshal.dump(v_proc)).call # >> TypeError (cannot dump Proc)

Defined Under Namespace

Modules: Isolatable, Marshalable, Parsers Classes: Binding, CannotAnalyseCodeError, CannotSerializeVariableError

Instance Method Summary collapse

Methods included from Marshalable

included

Constructor Details

#initialize(&block) ⇒ SerializableProc

Creates a new instance of SerializableProc by passing in a code block, in the process, all referenced variables (local, instance, class & global) within the block are extracted and isolated from the current context.

SerializableProc.new {|...| block }
x = lambda { ... }; SerializableProc.new(&x)
y = proc { ... }; SerializableProc.new(&y)
z = Proc.new { ... }; SerializableProc.new(&z)

The following will only work if u have ParseTree (not available for 1.9.* & JRuby) installed:

def action(&block) ; SerializableProc.new(&block) ; end
action { ... }

Fine-tuning of variables isolation can be done by declaring @@_not_isolated_vars within the code block:

SerializableProc.new do
  @@_not_isolated_vars = :global # don't isolate globals
  $stdout << 'WAKE UP !!'        # $stdout won't be isolated (avoid marshal error)
end

(see #call for invoking)



95
96
97
98
99
100
# File 'lib/serializable_proc.rb', line 95

def initialize(&block)
  file, line = /^#<Proc:0x[0-9A-Fa-f]+@(.+):(\d+).*?>$/.match(block.inspect)[1..2]
  @file, @line, @arity = File.expand_path(file), line.to_i, block.arity
  @code, @sexp = Parsers::PT.process(block) || Parsers::RP.process(self.class, @file, @line)
  @binding = Binding.new(block.binding, @sexp[:extracted])
end

Instance Method Details

#==(other) ⇒ Object

Returns true if other is exactly the same instance, or if other has the same string content.

x = SerializableProc.new { puts 'awesome' }
y = SerializableProc.new { puts 'wonderful' }
z = SerializableProc.new { puts 'awesome' }

x == x # >> true
x == y # >> false
x == z # >> true


114
115
116
117
# File 'lib/serializable_proc.rb', line 114

def ==(other)
  other.object_id == object_id or
    other.is_a?(self.class) && other.to_s == to_s
end

#arityObject

Returns the number of arguments accepted when running #call. This is extracted directly from the initializing code block, & is only as accurate as Proc#arity.

Note that at the time of this writing, running on 1.8.* yields different result from that of 1.9.*:

lambda { }.arity         # 1.8.* (-1) / 1.9.* (0)  (?!)
lambda {|x| }.arity      # 1.8.* (1)  / 1.9.* (1)
lambda {|x,y| }.arity    # 1.8.* (2)  / 1.9.* (2)
lambda {|*x| }.arity     # 1.8.* (-1) / 1.9.* (-1)
lambda {|x, *y| }.arity  # 1.8.* (-2) / 1.9.* (-2)
lambda {|(x,y)| }.arity  # 1.8.* (1)  / 1.9.* (1)


190
191
192
# File 'lib/serializable_proc.rb', line 190

def arity
  @arity
end

#bindingObject

:nodoc:

Raises:

  • (NotImplementedError)


232
233
234
# File 'lib/serializable_proc.rb', line 232

def binding #:nodoc:
  raise NotImplementedError
end

#call(*params) ⇒ Object Also known as: []

Just like the vanilla proc, invokes it, setting params as specified. Since the code representation of a SerializableProc is a lambda, expect lambda-like behaviour when wrong number of params are passed in.

SerializableProc.new{|i| (['hello'] * i).join(' ') }.call(2)
# >> 'hello hello'

In the case where variables have been declared not-isolated with @@_not_isolated_vars, invoking requires passing in Kernel.binding as the last parameter avoid unexpected surprises:

x, @x, @@x, $x = 'lx', 'ix', 'cx', 'gx'
s_proc = SerializableProc.new do
  @@_not_isolated_vars = :global, :class, :instance, :local
  [x, @x, @@x, $x].join(', ')
end

s_proc.call
# >> raises NameError for x
# >> @x is assumed nil (undefined)
# >> raises NameError for @@x (actually this depends on if u are using 1.9.* or 1.8.*)
# >> no issue with $x (since global is, after all, a global)

To ensure expected results:

s_proc.call(binding) # >> 'lx, ix, cx, gx'


222
223
224
225
226
227
228
# File 'lib/serializable_proc.rb', line 222

def call(*params)
  if (binding = params[-1]).is_a?(::Binding)
    to_proc(binding).call(*params[0..-2])
  else
    to_proc.call(*params)
  end
end

#to_proc(binding = nil) ⇒ Object

Returns a plain vanilla proc that works just like other instances of Proc, the only difference is that the binding of variables is the same as the serializable proc, which is isolated.

x, @x, @@x, $x = 'lx', 'ix', 'cx', 'gx'
s_proc = SerializableProc.new { [x, @x, @@x, $x].join(', ') }
x, @x, @@x, $x = 'ly', 'iy', 'cy', 'gy'
s_proc.to_proc.call # >> 'lx, ix, cx, gx'

Just like any object that responds to #to_proc, you can do the following as well:

def action(&block) ; yield ; end
action(&s_proc) # >> 'lx, ix, cx, gx'


134
135
136
137
138
139
140
# File 'lib/serializable_proc.rb', line 134

def to_proc(binding = nil)
  if binding
    eval(@code[:runnable], @binding.eval!(binding), @file, @line)
  else
    @proc ||= eval(@code[:runnable], @binding.eval!, @file, @line)
  end
end

#to_s(debug = false) ⇒ Object

Returns a string representation of itself, which is in fact the code enclosed within the initializing block.

SerializableProc.new { [x, @x, @@x, $x].join(', ') }.to_s
# >> lambda { [x, @x, @@x, $x].join(', ') }

By specifying debug as true, the true runnable code is returned, the only difference from the above is that the variables within has been renamed (in order to provide for variables isolation):

SerializableProc.new { [x, @x, @@x, $x].join(', ') }.to_s(true)
# >> lambda { [lvar_x, ivar_x, cvar_x, gvar_x].join(', ') }

The following renaming rules apply:

  • local variable -> prefixed with ‘lvar_’,

  • instance variable -> replaced ‘@’ with ‘ivar_’

  • class variable -> replaced ‘@@’ with ‘cvar_’

  • global variable -> replaced ‘$ with ’gvar_’



162
163
164
# File 'lib/serializable_proc.rb', line 162

def to_s(debug = false)
  @code[debug ? :runnable : :extracted]
end

#to_sexp(debug = false) ⇒ Object

Returns the sexp representation of this instance. By default, the sexp represents the extracted code, if debug specified as true, the runnable code version is returned.

SerializableProc.new { [x, @x, @@x, $x].join(', ') }.to_sexp


172
173
174
# File 'lib/serializable_proc.rb', line 172

def to_sexp(debug = false)
  @sexp[debug ? :runnable : :extracted]
end