reduxco 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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