/
environmented_proc.rb
158 lines (148 loc) · 6.51 KB
/
environmented_proc.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
##
# An `EnvironmentedProc` is a `Proc` extended to provide an alterable
# environment. Specifically, you can preform the following operations on an
# `EnvironmentedProc` in addition to those of a normal `Proc`:
#
# - Inject local variables into the execution scope of the `EnvironmentedProc`
# - Change the value of `self` for the duration of execution
#
# Downsides
# ---------
# It’s important to note that the method by which we preform these operations
# is rather fragile and hacky (and possibly, though I haven’t benchmarked
# `EnvironmentedProc` versus `Proc` yet, very slow). While these tools are
# very convenient, it would be prudent to avoid overusing them.
#
# Various things to be aware of:
#
# - Local variables wrapped into the scope of the block may override variables
# we inject into it (as true local variables override instance methods,
# which is what we are actually injecting into the scope of the block)
# - If the block depends on some minutiæ of the creation environment, there’s
# a chance it will break (as we override the creation environment to set
# `self`)
class EnvironmentedProc < Proc
# The value of `self` for the duration of execution
attr_accessor :self
def self; @self ||= binding.eval("self"); end
# A `Hash` of variables to be injected into the scope for the duration of
# execution.
attr_reader :variables
def variables; @variables ||= Hash.new; end
##
# `EnvironmentedProc`s are initialized with a block, an existing `Proc`
# object. When initialized, `self` is primed to the value of `self` in the
# scope of the existing `Proc`.
def initialize &block
super
self.self
end
##
# Injects objects into the scope of the `EnvironmentedProc` with the given
# variable name. Accepts a `Hash` of the form `{:variable_name => object}`.
#
# Variables may be, in fact, not only true variables, but also `Constants`,
# `@instance_variables`, and `@@class_variables`. All will be made available
# as their proper form.
#
# Example:
#
# eproc = ->{ p Foo, bar } % {Foo: 123, bar: 456}
# eproc.call
def inject variables
self.self = variables.delete(:self) if variables[:self]
self.variables.merge! variables
return self
end
alias_method :%, :inject
##
# Executes an `EnvironmentedProc` against the object stored in `self`.
# `variables` are stored in instance methods on the object stored in `self`
# before execution (any instance methods that would be overwritten are
# sequestered away and restored after execution, to avoid damaging the
# object in `self`).
def call(*args)
# Okay, if you’re reading this, you probably want to know how all this
# works. Uh… that’s gonna be fun /-:
# The problem is that variable/constant lookups seem to be handled in a
# different way for every single of the types of variables we’re trying
# to inject. I’ll describe how we set each, and why we’re setting it on
# the place we’re setting it on, below.
scope = Class.new
eigenclass = class<<self.self;self;end
reimplementables = variables.map do |name, object|
case name.to_s
when /^@@/
# Class variables are easily the weirdest of the bunch. Instead of
# being looked up in the scope of the closure (i.e. where the block is
# defined, which makes sense)… or in the scope of the object we’re
# evaluating the block on (`#self`, which makes a little less sense)…
# it’s looked up in the scope of *this function*, that is,
# `EnvironmentedProc#inject`. It boils down to looking in whatever
# scope the `#instance_eval` method is called in (not the object it’s
# called on, nor the scope of the block!) To circumvent this, we make
# the actual `#instance_eval` call inside an anonymous class, and we
# set our class variables on that class.
#
# This may change in the future, I very much expect this is a bug - as
# of this writing, I’m on:
# ruby 1.9.1p129 (2009-05-12 revision 23412) [i386-darwin10.0.0b1]
scope.class_variable_set(name, object)
[:class, name, nil]
when /^@/
# Instance variables are easier. Instance variable lookups happen in
# the same place as method lookups—the object (`#self`) on which we
# `#instance_eval` the block. We simply set the instance variable on
# that object for the duration of execution.
prior = self.self.instance_variable_get name
self.self.instance_variable_set name, object
[:instance, name, prior]
when /^[A-Z]/
# Constants are looked up in the inheritance tree of our `#self`; the
# most immediate place to temporarily define them is the singleton
# eigenclass.
prior = eigenclass.const_defined?(name) ?
eigenclass.const_get(name) : nil
eigenclass.const_set(name, object)
[:constant, name, prior]
else
# Finally, local variables are looked up in the definition scope of
# the block; however, it is difficult, messy, and evil to inject
# there. Since method calls use the same sentax as local variables,
# it’s much simpler to define a temporary instance method on `#self`,
# so this is exactly what we do.
umethod = eigenclass.method_defined?(name) ?
eigenclass.instance_method(name) : nil
eigenclass.send(:define_method, name) {object}
[:local, name, umethod]
end
end
proc = self
object = @self
rv = scope.module_eval { object.instance_exec(*args, &proc) }
reimplementables.each do |type, name, object|
# Now that we’ve evaluated the block, we’re going to teardown all the
# setup we preformed, resetting as much environment to its pre–execution
# state as possible.
case type
when :class
# We don’t re–set anything for class variables, because we set those
# on a throw–away anonymous `Class`, which will be destroyed at the
# closing of this method anyway.
when :instance
object ?
self.self.instance_variable_set(name, object) :
self.self.send(:remove_instance_variable, name)
when :constant
eigenclass.send(:remove_const, name)
eigenclass.const_set(name, object) if object
when :local
object ?
eigenclass.send(:define_method, name, &object) :
eigenclass.send(:remove_method, name)
end
end
return rv
end
alias_method :[], :call
end