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/marshalable.rb,
lib/serializable_proc/parsers/static.rb,
lib/serializable_proc/parsers/dynamic.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 = :all
  [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"

Supported values include :global, :class, :instance, :local & :all, with :all overriding all others. This can also be used as a workaround for variables that cannot be serialized:

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

Note that it is strongly-advised to append Kernel.binding as the last parameter when invoking the proc to avoid unnecessary nasty surprises. (see #call for more details)

#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 { ... }


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

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::Dynamic.process(block) || Parsers::Static.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


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

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)


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

def arity
  @arity
end

#bindingObject

:nodoc:

Raises:

  • (NotImplementedError)


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

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'


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

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'


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

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_’



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

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


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

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