Class: SerializableProc

Inherits:
Object
  • Object
show all
Includes:
Isolatable, Marshalable
Defined in:
lib/serializable_proc.rb,
lib/serializable_proc/binding.rb,
lib/serializable_proc/isolatable.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 = :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 Classes: Binding, CannotSerializeVariableError

Constant Summary

Constants included from Isolatable

Isolatable::BLOCK_SCOPES, Isolatable::ISOLATION_VAR, Isolatable::MAPPERS

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


89
90
91
92
93
94
95
96
# File 'lib/serializable_proc.rb', line 89

def initialize(&block)
  e_code, e_sexp = block.to_source, block.to_sexp
  r_sexp, r_code = isolated_sexp_and_code(e_sexp)
  @arity, @file, @line = block.arity, *block.source_location
  @codes = {:extracted => e_code, :runnable => r_code}
  @sexps = {:extracted => e_sexp, :runnable => r_sexp}
  @binding = Binding.new(block.binding, r_sexp)
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


110
111
112
113
# File 'lib/serializable_proc.rb', line 110

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)


186
187
188
# File 'lib/serializable_proc.rb', line 186

def arity
  @arity
end

#bindingObject

:nodoc:

Raises:

  • (NotImplementedError)


228
229
230
# File 'lib/serializable_proc.rb', line 228

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'


218
219
220
221
222
223
224
# File 'lib/serializable_proc.rb', line 218

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'


130
131
132
133
134
135
136
# File 'lib/serializable_proc.rb', line 130

def to_proc(binding = nil)
  if binding
    eval(@codes[:runnable], @binding.eval!(binding), @file, @line)
  else
    @proc ||= eval(@codes[: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_’



158
159
160
# File 'lib/serializable_proc.rb', line 158

def to_s(debug = false)
  @codes[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


168
169
170
# File 'lib/serializable_proc.rb', line 168

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