reduxco 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2012, WhitePages, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the company nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL WHITEPAGES, INC. BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.rdoc ADDED
@@ -0,0 +1,126 @@
1
+ = Overview
2
+
3
+ Reduxco is a general purpose graph reduction calculation engine for those
4
+ non-linear dependency flows that normal pipelines and Rack Middleware-like
5
+ architectures can't do cleanly.
6
+
7
+ Conceptually, it is similar to using Rack Middleware with named keys to store
8
+ intermediate calculations that have to be reused later, but unlike Rack Middleware,
9
+ Reduxco is self organizing based on the dependencies used by each piece.
10
+
11
+ It's primary public facing class is Reduxco::Context.
12
+
13
+ = Examples
14
+
15
+ == Basic Context Use
16
+
17
+ In practice, one can build one ore more tables of callable objects (e.g. Procs or
18
+ custom class instances), and register them with a Reduxco::Context. Callables can then
19
+ used their Reduco::Context handle to refer to the callables they can depend on.
20
+
21
+ For example, the addition of two numbers could be done as follows:
22
+
23
+ map = {
24
+ sum: ->(c){ c[:x] + c[:y] },
25
+ x: ->(c){ 3 },
26
+ y: ->(c){ 5 }
27
+ }
28
+
29
+ sum = Reduxco::Reduxer.new(map).reduce(:sum)
30
+ sum.should == 8
31
+
32
+ Note that the symbol <code>:app</code> is the default root node of Reduxco::Context#reduce,
33
+ so if <code>:sum</code> were renamed to <code>:app</code> above, the last line could
34
+ be slightly simplified as:
35
+
36
+ sum = Reduxco::Context.new(map).reduce
37
+
38
+ Of course, any object responding to <code>call</code> can be used as the values in
39
+ the map, so one could just as easily define a class with an instance method of
40
+ <code>call</code> on it instead of using Proc objects.
41
+
42
+ == Overriding and Super
43
+
44
+ If multiple maps of callables are given, and the keys (referred to as names from
45
+ here on) are duplicated in the maps, the last map given wins, shadowing the previous map.
46
+ For example:
47
+
48
+ map1 = {
49
+ message: ->(c){ 'Hello From Map 1' }
50
+ }
51
+
52
+ map2 = {
53
+ message: ->(c){ 'Hello From Map 2' }
54
+ }
55
+
56
+ msg = Reduxco::Reduxer.new(map1, map2).reduce(:message)
57
+ msg.should == 'Hello From Map 2'
58
+
59
+ If one wishes to refer to previous (shadowed) callables, one can do that using
60
+ Context#super. For example:
61
+
62
+ map1 = {
63
+ message: ->(c){ 'Hello From Map 1' }
64
+ }
65
+
66
+ map2 = {
67
+ message: ->(c){ c.super + ' and Hello From Map 2' }
68
+ }
69
+
70
+ msg = Reduxco::Context.new(map1, map2).reduce(:message)
71
+ msg.should == 'Hello From Map 1 and Hello From Map 2'
72
+
73
+ == Introspection
74
+
75
+ There are several introspection methods for making assertions about the
76
+ Reduxco::Context. These are usually used by callables to inspect their
77
+ environment before proceeding.
78
+
79
+ [Reduxco::Context#include?] Allows you to inspect if the Reduxco::Context
80
+ can resolve a given refname if called.
81
+ [Reduxco::Context#completed?] Allows you to inspect if the callable associated
82
+ with a given block name has already been called;
83
+ useful for assertions about weak dependencies.
84
+ [Reduxco::Context#assert_completed] Like <code>computed?</code>, but it raises
85
+ an exception if it fails.
86
+
87
+ == Before, After, and Inside
88
+
89
+ = Contact
90
+
91
+ Jeff Reinecke <jreinecke@whitepages.com>
92
+
93
+ = Roadmap
94
+
95
+ TBD
96
+
97
+ = History
98
+
99
+ [1.0.0 - 2013-Apr-??] Initial Release.
100
+
101
+ = License
102
+
103
+ Copyright (c) 2012, WhitePages, Inc.
104
+ All rights reserved.
105
+
106
+ Redistribution and use in source and binary forms, with or without
107
+ modification, are permitted provided that the following conditions are met:
108
+ * Redistributions of source code must retain the above copyright
109
+ notice, this list of conditions and the following disclaimer.
110
+ * Redistributions in binary form must reproduce the above copyright
111
+ notice, this list of conditions and the following disclaimer in the
112
+ documentation and/or other materials provided with the distribution.
113
+ * Neither the name of the company nor the
114
+ names of its contributors may be used to endorse or promote products
115
+ derived from this software without specific prior written permission.
116
+
117
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
118
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
119
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
120
+ DISCLAIMED. IN NO EVENT SHALL WHITEPAGES, INC. BE LIABLE FOR ANY
121
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
122
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
123
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
124
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
125
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
126
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'pathname'
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../Gemfile", Pathname.new(__FILE__).realpath)
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require "rspec/core/rake_task"
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.rspec_opts = ['--backtrace']
10
+ end
11
+
12
+ require 'rdoc/task'
13
+
14
+ RDoc::Task.new do |rdoc|
15
+ rdoc.rdoc_dir = "rdoc"
16
+ rdoc.rdoc_files.add "lib/**/*.rb", "README.rdoc"
17
+ rdoc.options << "--all"
18
+ #rdoc.options << "--coverage-report" # Useful for finding something undocumented, but won't generate output when this is selected!
19
+ end
data/lib/reduxco.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'reduxco/version'
2
+
3
+ require 'reduxco/callable_ref'
4
+ require 'reduxco/context'
5
+ require 'reduxco/reduxer'
6
+
7
+ # See README.rdoc or Reduxco::Reduxer
8
+ module Reduxco
9
+ # Only declared here for RDoc documentation purposes of the module.
10
+ end
@@ -0,0 +1,142 @@
1
+ module Reduxco
2
+ # An immutable class that represents a referrence to a callable in a
3
+ # CallableTable; this class is rarely used directly by clients.
4
+ class CallableRef
5
+ include Comparable
6
+
7
+ # The minimum depth number allowed.
8
+ MIN_DEPTH = 1
9
+
10
+ # For string representations (typically used in debugging), this is
11
+ # used as the separator between name and depth (if depth is given).
12
+ STR_SEPARATOR = ':'
13
+
14
+ # For string representations, what is the opening bracket string.
15
+ STR_LEFT_BRACKET = '<'
16
+
17
+ # For string representations, what is the opening bracket string.
18
+ STR_RIGHT_BRACKET = '>'
19
+
20
+ # [name] Typically the name is a symbol, but systems are free to use other
21
+ # objects as types are not coerced into other types at any point.
22
+ # If the name is a CallableRef, then this acts as a copy constructor.
23
+ #
24
+ # [depth] The depth is normally not given when used, can be specified for
25
+ # referencing specific shadowed callables when callables are flattend
26
+ # into a CallableTable; this is important for calls to super.
27
+ def initialize(name, depth=nil)
28
+ case name
29
+ when self.class
30
+ @name = name.name
31
+ @depth = (depth && depth.to_i) || name.depth
32
+ else
33
+ @name = name
34
+ @depth = depth && depth.to_i
35
+ end
36
+
37
+ raise IndexError, "Depth must be greater than zero", caller if depth && depth<MIN_DEPTH
38
+ end
39
+
40
+ # Returns the name of the refernce.
41
+ attr_reader :name
42
+
43
+ # Returns the depth of the reference, or nil if the reference is dynamic.
44
+ attr_reader :depth
45
+
46
+ # Is true valued when the reference will dynamically bind to an entry
47
+ # in the CallableTable instead of to an entry at a specific depth.
48
+ def dynamic?
49
+ return depth.nil?
50
+ end
51
+
52
+ # Negation of dynamic?
53
+ def static?
54
+ return !dynamic?
55
+ end
56
+
57
+ # Returns a CallableRef with the same name, but one depth deeper.
58
+ def succ
59
+ if( dynamic? )
60
+ raise RuntimeError, "Dynamic references cannot undergo relative movement."
61
+ else
62
+ self.class.new(name, depth.succ)
63
+ end
64
+ end
65
+ alias_method :next, :succ
66
+
67
+ # Returns a CallableRef with the same name, but one depth higher.
68
+ def pred
69
+ if( dynamic? )
70
+ raise RuntimeError, "Dynamic references cannot undergo relative movement."
71
+ else
72
+ self.class.new(name, depth.pred)
73
+ end
74
+ end
75
+
76
+ # Returns a unique hash value; useful resolving Hash entries.
77
+ def hash
78
+ @hash ||= self.to_a.hash
79
+ end
80
+
81
+ # Returns true if the passed ref is
82
+ #
83
+ # This method raises an exception when compared to anything that does not
84
+ # ducktype as a reference.
85
+ def include?(other)
86
+ other.name == self.name && (dynamic? ? true : other.depth == self.depth)
87
+ end
88
+
89
+ # Returns true if the refs are equivalent.
90
+ def eql?(other)
91
+ if( other.kind_of?(CallableRef) || (other.respond_to?(:name) && other.respond_to?(:depth)) )
92
+ other.name == self.name && other.depth == self.depth
93
+ else
94
+ false
95
+ end
96
+ end
97
+ alias_method :==, :eql?
98
+ alias_method :===, :==
99
+
100
+ # Returns the sort order of the reference. This is primarily useed
101
+ # for sorting references in CallableTable so that shadowed callables
102
+ # are called properly.
103
+ #
104
+ # Static references are sorted by the following rule: For all sets of static
105
+ # refs with equal names, sort by depth. For all sets of static refs with
106
+ # equal depths, only sort if the names are sortable. This means that
107
+ # there is no requirement for sort order to group by name or by depth, and
108
+ # so no software should be written around an assumption of which comes first.
109
+ #
110
+ # Refuses to sort dynamic references, as they are not ordered compared to
111
+ # static references.
112
+ def <=>(other)
113
+ if( dynamic? != other.dynamic? )
114
+ nil
115
+ else
116
+ depth_eql = depth <=> other.depth
117
+ (depth_eql==0 ? (name <=> other.name) : nil) || depth_eql
118
+ end
119
+ end
120
+
121
+ # Returns an array form of this CallableReference.
122
+ def to_a
123
+ @array ||= [name, depth]
124
+ end
125
+
126
+ # Returns a hash form of this CallableReference.
127
+ def to_h
128
+ @hash ||= {name:name, depth:depth}
129
+ end
130
+
131
+ # Returns a human readable string form of this CallableReference.
132
+ def to_s
133
+ @string ||= STR_LEFT_BRACKET + self.to_a.compact.map {|prop| prop.to_s}.join(STR_SEPARATOR) + STR_RIGHT_BRACKET
134
+ end
135
+
136
+ # Returns a human readable string form of this CallableReference.
137
+ def inspect
138
+ to_s
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,243 @@
1
+ require_relative 'callable_ref'
2
+ require_relative 'context/callable_table'
3
+ require_relative 'context/callstack'
4
+
5
+ module Reduxco
6
+ # Context is the client facing object for Reduxco.
7
+ #
8
+ # Typically, one instantiates a Context with one or more maps of callables
9
+ # by name, and then calls Contxt#reduce to calculate all dependent nodes and return
10
+ # a result.
11
+ #
12
+ # Maps may be any object that, when iterated with each, gives name/callable
13
+ # pairs. Names may be any object that can serve as a hash key. Callables can
14
+ # be any object that responds to the call method.
15
+ #
16
+ # == Overview
17
+ #
18
+ # Context orchestrates the reduction calculation. It is primarily used
19
+ # by callables invoked during computation to get access to their environment.
20
+ #
21
+ # Instantiators of a Context typically only use the Context#reduce method.
22
+ #
23
+ # Users of Reduxco should use Reduxco::Reduxer rather than directly consume
24
+ # Context directly.
25
+ #
26
+ # == Callable Helper Functions
27
+ #
28
+ # Callables (objects that respond to call) are the meat of the Context.
29
+ # When their call method is invoked, it is passed a reference to the Context.
30
+ # Callables can use this reference to access a range of methods, including
31
+ # the following:
32
+ #
33
+ # [Context#call] Given a refname, run the associated callable and returns
34
+ # its value. Usually invoked as Context#[]
35
+ # [Context#include?] Introspects if a refname is available.
36
+ # [Context#completed?] Instrospects if a callable has been called and returned.
37
+ # [Context#after] Given a refname and a block, runs the contents of the block
38
+ # after the given refname, but returns the value of the callable
39
+ # accociated with the refname.
40
+ # [Context#inside] Given a refname and a block, runs the callable associated
41
+ # with the refname, giving it access to running the block
42
+ # inside of it and getting its value.
43
+ class Context
44
+
45
+ # Special error type for halting due to a cyclic graph.
46
+ class CyclicalError < StandardError; end
47
+
48
+ # Special error type for when the callable provided has no call method.
49
+ class NotCallableError < NoMethodError; end
50
+
51
+ # A namespaced NameError for when the callref cannot be resolved.
52
+ class NameError < ::NameError; end
53
+
54
+ # Special error type when Context assert methods fail. See +assert_computed+.
55
+ class AssertError < StandardError; end
56
+
57
+ # A namespaced LocalJumpError for when no block is given but a yield is called.
58
+ class LocalJumpError < ::LocalJumpError; end
59
+
60
+ # Instantiate a Context with the one or more callalbe maps (e.g. hashes
61
+ # whose keys are names and values are callable) for calculations.
62
+ #
63
+ # The further to the right in the arguments that a map is, the higher
64
+ # the precedence of itsdefinition.
65
+ def initialize(*callable_maps)
66
+ @callable_maps = callable_maps
67
+ @calltable = CallableTable.new(@callable_maps)
68
+
69
+ @callstack = Callstack.new
70
+ @cache = {}
71
+
72
+ @block_association_cache = {}
73
+ end
74
+
75
+ # Given a refname, call it for this context and return the result.
76
+ #
77
+ # This can also take CallableRef instances directly, however if you find
78
+ # yourself passing in static references, this is likely because of design
79
+ # flaw in your callable map hierarchy.
80
+ #
81
+ # Call results are cached so that their values can be re-used. If callables
82
+ # have side-effects their side-effects are only invoked the first time
83
+ # they are run.
84
+ #
85
+ # Given a block, Context#yield may be used by the callable to invoke the
86
+ # block. Depending on the purpose of the block, Context#inside may be
87
+ # the preferrable alias.
88
+ def call(refname=:app, &block)
89
+ # First, we resolve the callref and invoke it.
90
+ frame, callable = @calltable.resolve( CallableRef.new(refname) )
91
+
92
+ # If the ref is nil then we couldn't resolve, otherwise invoke.
93
+ if( frame.nil? )
94
+ raise NameError, "No reference for name #{refname.inspect}", caller
95
+ else
96
+ invoke(frame, callable, &block)
97
+ end
98
+ end
99
+ alias_method :reduce, :call
100
+
101
+ # Shorthand for call, [] is the most frequently used form of the call method
102
+ # due to its abbreviated form..
103
+ alias_method :[], :call
104
+
105
+ # Inside is preferred to call when call is given a block and the intent
106
+ # is to manage flow control. Compare to Context#before and Context#after.
107
+ alias_method :inside, :call
108
+
109
+ # When invoked, finds the next callable in the CallableTable up the chain
110
+ # from the current frame, calls it, and returns the result.
111
+ #
112
+ # This is primarily used to reference shadowed callables in their overrides.
113
+ #
114
+ # Like Context#call, it may take a block that is yielded to with
115
+ # Context#yield. If no block is given but the current scope has a block,
116
+ # its block will be automatically forwarded.
117
+ def super(&block)
118
+ # First, we resolve the super ref.
119
+ frame, callable = @calltable.resolve_super( current_frame )
120
+
121
+ # If the ref is nil then we couldn't resolve, otherwise invoke.
122
+ if( frame.nil? )
123
+ raise NameError, "No super found for #{current_frame}", caller
124
+ else
125
+ block = block_for_frame(current_frame) if block.nil?
126
+ invoke(frame, callable, &block)
127
+ end
128
+ end
129
+
130
+ # Yields to the block given to a #Context.call
131
+ def yield(*args)
132
+ block = block_for_frame(current_frame)
133
+ if( block.nil? )
134
+ raise LocalJumpError, "No block given to yield to.", caller
135
+ else
136
+ block.yield(*args)
137
+ end
138
+ end
139
+
140
+ # Returns a copy of the current callstack.
141
+ def callstack
142
+ @callstack.dup
143
+ end
144
+
145
+ # Returns the top frame of the callstack.
146
+ def current_frame
147
+ @callstack.top
148
+ end
149
+
150
+ # Returns a true value if the given refname is defined in this context.
151
+ #
152
+ # If given a CallableRef, it returns a true value if the reference is
153
+ # resolvable.
154
+ def include?(refname)
155
+ @calltable.resolve( CallableRef.new(refname) ) != CallableTable::RESOLUTION_FAILURE
156
+ end
157
+
158
+ # Returns a true value if the given refname has been computed.
159
+ #
160
+ # If the given CallableRef, it returns a true if the reference has already
161
+ # been computed.
162
+ def completed?(refname)
163
+ callref = CallableRef.new(refname)
164
+ key = callref.dynamic? ? @calltable.resolve(callref).first : callref
165
+ @cache.include?(key)
166
+ end
167
+
168
+ # Raises an exception if +completed?+ is false. Useful for asserting weak
169
+ # dependencies (those which you do not need the return value of) have
170
+ # been met.
171
+ def assert_completed(refname)
172
+ raise AssertError, "Assertion that #{refname} has completed failed.", caller unless completed?(refname)
173
+ end
174
+
175
+ # Runs the passed block before calling the passed refname. Returns the
176
+ # value of the call to refname.
177
+ def before(refname)
178
+ yield if block_given?
179
+ call(refname)
180
+ end
181
+
182
+ # Runs the passed block after calling the passed refname. Returns the
183
+ # value of the call to refname.
184
+ def after(refname)
185
+ result = call(refname)
186
+ yield if block_given?
187
+ result
188
+ end
189
+
190
+ # Duplication of Contexts are dangerous because of all the deeply
191
+ # nested structures. That being said, it is very tempting to try
192
+ # to use a well-constructed Context rather than save and reuse the
193
+ # callable maps used for instantiation.
194
+ #
195
+ # To remedy this concern, dup acts as a copy constructor, making a new
196
+ # Context instance with the same callable maps, but is otherwise
197
+ # freshly constructed.
198
+ def dup
199
+ self.class.new(*@callable_maps)
200
+ end
201
+
202
+ private
203
+
204
+ # Invoke is the root method for all invocation of callables.
205
+ #
206
+ # It is given the frame to put on the stack (typically just a CallableRef),
207
+ # and the callable to invoke.
208
+ #
209
+ # It is up to the callers of this method to resolve the callable that must
210
+ # be called, or give a Reduxco::Context::NameError if it cannot be found.
211
+ def invoke(frame, callable, &block)
212
+ #Push the frame onto the callstack.
213
+ @callstack.push(frame)
214
+
215
+ # Once we've added the frame to the callstack, we MUST do all work in
216
+ # a begub/ensure so that exception handling callables get a consistent
217
+ # callstack!
218
+ begin
219
+ # If the ref is already in the stack, then we have a cyclical dependency.
220
+ if( @callstack.rest.include?(frame) )
221
+ raise CyclicalError, "Cyclical dependency on #{frame.inspect} in #{@callstack.rest.top.inspect}", callstack.to_caller(caller[1])
222
+ end
223
+
224
+ # Recall from cache, or build if necessary.
225
+ unless( @cache.include?(frame) )
226
+ @block_association_cache[frame] = block
227
+ @cache[frame] = callable.respond_to?(:call) ? callable.call(self) : raise(NotCallableError, "#{frame} does not resolve to a callable.", caller[1..-1])
228
+ end
229
+ @cache[frame]
230
+ ensure
231
+ # No matter what crashes happened, we must ensure we pop the frame off the stack.
232
+ popped = @callstack.pop
233
+ end
234
+ end
235
+
236
+ # Returns the block argument given to the Context#call for the given frame,
237
+ # or nil if no block was given.
238
+ def block_for_frame(frame)
239
+ @block_association_cache[frame]
240
+ end
241
+
242
+ end
243
+ end