leap 0.4.6 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -20,3 +20,4 @@ pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
22
  Gemfile.lock
23
+ .yardoc
@@ -0,0 +1,250 @@
1
+ # Leap
2
+
3
+ See also: [RDoc](http://rubydoc.info/github/rossmeissl/leap/master/frames).
4
+
5
+ Leap is a system for:
6
+
7
+ 1. describing decision-making strategies used to determine a potentially non-obvious attribute of an object
8
+ 2. computing that attribute by choosing appropriate strategies given a specific set of input information
9
+
10
+ At [Brighter Planet](http://brighterplanet.com), we use Leap to determine the carbon footprint of activities like flights that produce some quantity of CO2 emissions. Given, for example, an arbitrary real-life flight, for which we know some subset of its real-life details, Leap uses the most appropriate methodology to arrive at a result.
11
+
12
+ ## Quick start and silly example
13
+
14
+ ``` console
15
+ $ gem install leap
16
+ ```
17
+
18
+ ``` ruby
19
+ class Person
20
+ # attributes: name, age, gender
21
+
22
+ include Leap
23
+ decide :lucky_number do
24
+ committee :lucky_number do
25
+ quorum 'look it up in the magic book', :needs => [:name, :age] do |characteristics|
26
+ MagicBook.lookup(characteristics[:name], characteristics[:age]).lucky_number
27
+ end
28
+ quorum 'from lucky color', :needs => :lucky_color do |characteristics|
29
+ characteristics[:lucky_color].chars.first.unpack('C')
30
+ end
31
+ end
32
+
33
+ committee :lucky_color do
34
+ quorum 'guess by gender', :needs => :gender do |characteristics|
35
+ case characteristics[:gender].to_sym
36
+ when :male
37
+ 'blue'
38
+ when :female
39
+ 'pink'
40
+ end
41
+ end
42
+ quorum 'guess by age', :needs => :age do |characteristics|
43
+ if 14..17.include? characteristics[:age]
44
+ 'black'
45
+ elsif characteristics[:age] < 5
46
+ 'rainbow'
47
+ end
48
+ end
49
+ end
50
+
51
+ committee :gender do
52
+ quorum 'look up in a name book', :needs => :name do |characteristics|
53
+ NameBook.lookup(characteristics[:name]).gender
54
+ end
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ ``` irb
61
+ > Person.new(:age => 28).lucky_number
62
+ => 42
63
+ ```
64
+
65
+
66
+ ## Extended example
67
+
68
+ Let's say that somewhere in our Ruby application we need to figure out what time of day it is. Because time of day depends on a frame of reference, we'll consider it as a computable property of a Person.
69
+
70
+ ``` ruby
71
+ class Person < ActiveRecord::Base # using Rails, which is totally optional
72
+ has_one :watch
73
+ end
74
+ ```
75
+
76
+ Obviously if you have a watch, this is a pretty straightforward problem:
77
+
78
+ ``` ruby
79
+ class Person
80
+ has_one :watch
81
+
82
+ include Leap
83
+ decide :time do
84
+ committee :time do
85
+ quorum 'look at your watch', :needs => :watch do |characteristics|
86
+ characteristics[:watch].time
87
+ end
88
+ end
89
+ end
90
+ end
91
+ ```
92
+ The `decide :time do . . . end` block is called the *decision*. In this case, we're describing how to decide *time* for a given Person; we say that time is the *goal* of the decision. Leap will define a method on the object named after the decision (in this case, `Person#time`). Calling this method computes the decision.
93
+
94
+ The `committee :time do . . . end` block is called a *committee*. Committees represent a set of methodologies, all of which are appropriate means of determining its namesake property, in this case *time*. All decision blocks must at the very least provide a committee for the goal; this is implicitly the *master committee*. Typically Leap decisions will involve many more committees, each of which is tasked with determining some intermediate result necessary for the master committee to arrive at a conclusion.
95
+
96
+ The `quorum 'look at your watch' . . . end` block is called a *quorum*. Quorums describe *one particular way* of reaching the committee's conclusion, along with a list (`:needs`) of what they need to be employed. The `characteristics` blockvar is a curated subset of the object's attributes presented to the quorum (which is in this sense a *closure*) for consideration.
97
+
98
+ Back to the example. Having a watch does indeed make telling time easy. Complexities arise when you don't; you have to fallback to more intuitive methods.
99
+
100
+ For example, we can look at the angle of the sun. (I'm going to start abbreviating the code blocks at this point.)
101
+
102
+ ``` ruby
103
+ decide :time do
104
+ committee :time do
105
+ quorum 'look at your watch', :needs => :watch do |characteristics|
106
+ characteristics[:watch].time
107
+ end
108
+ quorum 'use a sun chart', :needs => [:solar_elevation, :location] do |characteristics|
109
+ SunChart.for(characteristics[:location]).solar_elevation_to_time(characteristics[:solar_elevation])
110
+ end
111
+ end
112
+ end
113
+ ```
114
+
115
+ But how do we know solar elevation? Or location for that matter? Committees! (Note that committees are always convened in reverse order, from last to first.)
116
+
117
+ ``` ruby
118
+ decide :time do
119
+ committee :time do
120
+ quorum 'look at your watch', :needs => :watch # . . .
121
+ quorum 'use a sun chart', :needs => [:solar_elevation, :location] # . . .
122
+ end
123
+ committee :solar_elevation do
124
+ quorum 'use a solar angle gauge', :needs => :solar_angle_gauge # . . .
125
+ quorum 'ask a friend', :needs => :friend # . . .
126
+ end
127
+ committee :location do
128
+ quorum 'find out from your GPS', :needs => :gps_device # . . .
129
+ quorum 'geocode it', :needs => :current_address # . . .
130
+ end
131
+ end
132
+ ```
133
+
134
+ Surely there is more than one way of determining the Person's current address, so, as you can see, a dozen or more committees, each with several quorums, would be necessary to completely describe the intuitive process that we humans use to figure out something as simple as the likely time of day. This is a useful way to think about Leap: it's a non-learning artifical intelligence system that attempts to model human intuition by describing heuristic strategies.
135
+
136
+ Now that we've looked at an overview of the Leap system, let's look at each component in depth, from the inside (characteristics) out (decisions).
137
+
138
+ ## Characteristics
139
+
140
+ The computation of a Leap decision requires a curated set of properties of the host object. In the examples above, you'll see that each quorum receives a `characteristics` hash. Where does this come from?
141
+
142
+ By default, Leap will dynamically construct a characteristics hash out of the object's instance variables. This is definitely a stopgap solution though; much better is to provide a method on the object that returns a *curated* set of properties ("characteristics") and specify it to Leap using the `:with` option on the `decide` block:
143
+
144
+ ``` ruby
145
+ class Person < ActiveRecord::Base
146
+ def characteristics
147
+ attributes.slice :name, :age, :gender
148
+ end
149
+
150
+ include Leap
151
+ decide :lucky_number, :with => :characteristics
152
+ # . . .
153
+ end
154
+ end
155
+ ```
156
+
157
+ Even better is to use a library like [Charisma](http://github.com/brighterplanet/charisma) that does the curation for you:
158
+
159
+ ``` ruby
160
+ class Person < ActiveRecord::Base
161
+ include Charisma
162
+ characterize do
163
+ has :name
164
+ has :age
165
+ has :gender
166
+ end
167
+
168
+ include Leap
169
+ decide :lucky_number, :with => :characteristics
170
+ # . . .
171
+ end
172
+ end
173
+ ```
174
+
175
+ When it comes time to compute your decision block, Leap will call your characteristics method, duplicate the resulting hash, and send it into the decision. The hash gets handed from one committee to the next, with each committee inserting its conclusion. In this way, the hash accumulates increasing knowledge about the object until it reaches the master committee for final determination.
176
+
177
+ It's worth noting that often a decision block will have committees that correspond exactly (by name) with attributes in the characteristics hash. That's because there's never a guarantee that all of the object's attributes will be non-nil. If an attribute is directly provided by the object, the corresponding committee will never be called.
178
+
179
+ ## Defining quorums
180
+
181
+ Quorums are encapsulated methodologies for determining something, given a
182
+ specific set of inputs.
183
+
184
+ ``` ruby
185
+ class Automobile
186
+ include Leap
187
+ decide :total_cost_of_ownership do
188
+ # . . .
189
+ committee :annual_insurance_premium do
190
+ # . . .
191
+ quorum 'from type of automobile', :needs => [:make, :model], :appreciates => :color do |characteristics|
192
+ premium = Automobile.scoped(:make => characteristics[:make], :model => characteristics[:model]).average(:annual_claims_total) * 1.2 # profit!
193
+ premium += 100 if characteristics[:color] == 'red'
194
+ premium
195
+ end
196
+ # . . .
197
+ end
198
+ # . . .
199
+ end
200
+ end
201
+ ```
202
+
203
+ This is an example from a total cost of ownership model. In this case, the annual insurance premium can, among other ways, be calculated from the type of automobile. In order for this methodology to be employable, we need to know, or Leap must have already determined (by a prior committee) the automobile's make and model. If it's available, the automobile's color will also help.
204
+
205
+ The quorum block gets stored as a closure for later use; the `:needs` act as a gatekeeper. If, during computation, available characteristics satisfy the quorum's requirements, then the *subset* of those characteristics that are actually *asked for* (via `:needs` or `:appreciates`) is provided to the block.
206
+
207
+ ## Committees
208
+
209
+ Committees are charged with producing a single value, as defined by its name, and, in pursuit of this value, identifying and acknowledging its most appropriate quorum to return this value, given available input information.
210
+
211
+ Quorums are always considered top-to-bottom. When called, the committee assesses each quorum, checking to see if its needs are satisfied by available information. The first satisfied quorum is acknowledged. If its conclusion is non-nil, it becomes the committee's conclusion; if it is nil, then the committee continues to the next satisfied quorum. If no conclusion is possible, the committee returns a nil result.
212
+
213
+ ## Decisions
214
+
215
+ Decisions are ordered groups of committees fashioned to provide a number of pathways to a single result (the goal). Committees are always called bottom-to-top, so that progressively more information becomes known about the object, ultimately reaching the master committee that provides the result of the decision. Leap begins with the curated characteristics hash described above. It submits this hash to the first committee (the bottom one), which produces some result. This result is added to the characteristics hash, which is then submitted to the next committee, and so on, until the final (top) committee is called, benefiting from all of the intermediate committee conclusions.
216
+
217
+ ## Compliance
218
+
219
+ Leap is all about providing numerous methods for arriving at a conclusion. But inevitably that means that some <del>unsavory</del> unorthodox quorums will make their way into your committees, and sometimes you'll want to restrict your decisions to conventional approaches. Leap supports this with its *compliance system*.
220
+
221
+ Just mark certain quorums as complying with certain protocols:
222
+
223
+ ``` ruby
224
+ quorum 'outside the box', :needs => :type do |characteristics|
225
+ # . . .
226
+ end
227
+ quorum 'mind the red tape', :needs => :type, :complies => :the_rules do |characteristics|
228
+ # . . .
229
+ end
230
+ ```
231
+
232
+ and then perform your decision with a protocol constraint:
233
+
234
+ ``` irb
235
+ > Person.lucky_number :comply => :the_rules
236
+ => 99
237
+ ```
238
+
239
+ You can name your protocols how ever you want; they just have to match between the quorum assertions and the decision option.
240
+
241
+ ## Internals
242
+
243
+ Normally you'll construct your decision strategies using `decide :foo . . . end` blocks and perform them using the resulting `#foo` methods, but sometimes you'll want access to Leap's internals.
244
+
245
+ * All decisions are stored as Leap::Decision objects in the host class's `@decisions` variable. Leap provides a `#decisions` accessor.
246
+ * When a decision is *computed* for a specific instance, it is stored as a Leap::Deliberation in the instance's `@deliberations` variable. Leap provides a `#deliberations` accessor.
247
+
248
+ ## Copyright
249
+
250
+ Copyright (c) 2010 Andy Rossmeissl.
data/Rakefile CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
3
 
4
+ require 'bueller'
5
+ Bueller::Tasks.new
6
+
4
7
  require 'rake'
5
8
  require 'rake/testtask'
6
9
  Rake::TestTask.new(:test) do |test|
@@ -8,6 +11,7 @@ Rake::TestTask.new(:test) do |test|
8
11
  test.pattern = 'test/**/test_*.rb'
9
12
  test.verbose = true
10
13
  end
14
+ task :default => :test
11
15
 
12
16
  begin
13
17
  require 'rake/rdoctask'
@@ -1,27 +1,28 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "leap/version"
4
-
5
- Gem::Specification.new do |s|
6
- s.name = "leap"
7
- s.version = Leap::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = ["Andy Rossmeissl", "Seamus Abshere"]
10
- s.email = "andy@rossmeissl.net"
11
- s.homepage = "http://github.com/rossmeissl/leap"
12
- s.summary = %Q{A heuristics engine for your Ruby objects}
13
- s.description = %Q{Leap to conclusions}
14
-
15
- s.files = `git ls-files`.split("\n")
16
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
- s.require_paths = ["lib"]
19
-
20
- s.add_development_dependency "characterizable", ">=0.0.11"
21
- s.add_development_dependency "shoulda"
22
- s.add_dependency 'blockenspiel', '>=0.3.2'
23
- s.add_dependency 'activesupport', '>=2.3.4'
24
- # sabshere 1/27/11 for activesupport - http://groups.google.com/group/ruby-bundler/browse_thread/thread/b4a2fc61ac0b5438
25
- s.add_dependency 'i18n'
26
- s.add_dependency 'builder'
27
- end
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "leap/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "leap"
7
+ s.version = Leap::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Andy Rossmeissl", "Seamus Abshere", "Derek Kastner"]
10
+ s.email = "andy@rossmeissl.net"
11
+ s.homepage = "http://github.com/rossmeissl/leap"
12
+ s.summary = %Q{A heuristics engine for your Ruby objects}
13
+ s.description = %Q{Leap to conclusions}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_development_dependency "charisma", '~>0.2.0'
21
+ s.add_development_dependency "shoulda"
22
+ s.add_development_dependency 'bueller'
23
+ s.add_dependency 'blockenspiel', '>=0.3.2'
24
+ s.add_dependency 'activesupport', '>=2.3.4'
25
+ # sabshere 1/27/11 for activesupport - http://groups.google.com/group/ruby-bundler/browse_thread/thread/b4a2fc61ac0b5438
26
+ s.add_dependency 'i18n'
27
+ s.add_dependency 'builder'
28
+ end
@@ -9,7 +9,6 @@ end if ActiveSupport::VERSION::MAJOR == 3
9
9
  require 'blockenspiel'
10
10
 
11
11
  require 'leap/core_ext'
12
- require 'leap/xml_serializer'
13
12
  require 'leap/subject'
14
13
  require 'leap/committee'
15
14
  require 'leap/quorum'
@@ -17,11 +16,16 @@ require 'leap/decision'
17
16
  require 'leap/report'
18
17
  require 'leap/deliberation'
19
18
  require 'leap/implicit_attributes'
19
+ require 'leap/no_solution_error'
20
+ require 'leap/deliberations_accessor'
21
+ require 'leap/goal_methods_documentation'
20
22
 
23
+ # Leap is a system for: 1) describing decision-making strategies used to determine a potentially non-obvious attribute of an object and 2)
24
+ # computing that attribute by choosing appropriate strategies given a specific set of input information
21
25
  module Leap
26
+ # Injects <tt>Leap::Subject</tt> into the host class
27
+ # @see Subject
22
28
  def self.included(base)
23
29
  base.extend ::Leap::Subject
24
30
  end
25
-
26
- class NoSolutionError < ArgumentError; end
27
31
  end
@@ -1,44 +1,70 @@
1
1
  module Leap
2
+ # One of the basic building blocks of a Leap decision.
3
+ #
4
+ # Committees represent computable attributes of a subject instance and facilitate multiple means of computing these attributes. In some cases, you will
5
+ # define committees that match the names of attributes that can be explicitly set on a subject; in this case, the committees are only useful when
6
+ # a particular instance does not have this attribute set. In other cases, committees are part of internal logic within the Leap decision, providing
7
+ # intermediate results in pursuit of the decision's ultimate goal.
8
+ #
9
+ # As a Leap decision is made, it approaches each committee in turn, providing it with its current knowledge of the subject. The committee accepts this information, determines an appropriate methodology, and returns additional information about the subject. This newly-augmented knowledge about the subject is then passed to the next committee for further augmentation, and so forth.
10
+ #
11
+ # Committees are composed of one or more quorums (<tt>Leap::Quorum</tt> objects) that encapsulate specific methodologies for drawing the committee's conclusion. This composition is defined within the <tt>Leap::Decision#committee</tt> block using the <tt>Leap::Committee#quorum</tt> DSL.
12
+ # @see Leap::Quorum
2
13
  class Committee
3
- include XmlSerializer
4
14
 
5
- attr_reader :name, :quorums
15
+ # The name of the committee, traditionally formulated to represent a computable attribute of the subject.
16
+ attr_reader :name
6
17
 
18
+ # The array of quorums defined within the committee.
19
+ attr_reader :quorums
20
+
21
+ # Creates a new <tt>Leap::Committee</tt> with the given name.
22
+ #
23
+ # Generally you don't create these objects directly; <tt>Leap::Decision#committee</tt> does that for you.
24
+ #
25
+ # @see Leap::Decision#committee
26
+ # @param [Symbol] name The name of the committee, traditionally formulated to represent a computable attribute of the subject.
7
27
  def initialize(name)
8
28
  @name = name
9
29
  @quorums = []
10
30
  end
11
31
 
12
- def report(subject, characteristics, considerations, options = {})
32
+ # Instructs the committee to draw its conclusion.
33
+ #
34
+ # Steps through each quorum defined within the committee in search of one that can be called with the provided characteristics and is compliant with the requested protocols. Upon finding such a quorum, it is called. If a conclusion is returned, it becomes the committee's conclusion; if not, the quorum-seeking process continues.
35
+ #
36
+ # Generally you will not call this method directly; <tt>Leap::Decision#make</tt> requests reports at the appropriate time.
37
+ # @param [Hash] characteristics What we know so far about the subject.
38
+ # @param [Array] considerations Auxiliary contextual details about the decision (see <tt>Leap::GoalMethodsDocumentation</tt>)
39
+ # @param [optional, Hash] options
40
+ # @option comply Compliance constraint. See <tt>Leap::GoalMethodsDocumentation</tt>.
41
+ # @see Leap::GoalMethodsDocumentation
42
+ # @return Leap::Report
43
+ def report(characteristics, considerations, options = {})
13
44
  quorums.grab do |quorum|
14
45
  next unless quorum.satisfied_by? characteristics and quorum.complies_with? Array.wrap(options[:comply])
15
46
  if conclusion = quorum.acknowledge(characteristics.slice(*quorum.characteristics), considerations.dup)
16
- ::Leap::Report.new subject, self, quorum => conclusion
17
- end
18
- end
19
- end
20
-
21
- def as_json(*)
22
- {
23
- 'name' => name.to_s,
24
- 'quorums' => quorums.map(&:as_json)
25
- }
26
- end
27
-
28
- def to_xml(options = {})
29
- super options do |xml|
30
- xml.committee do |committee_block|
31
- committee_block.name name.to_s, :type => 'string'
32
- array_to_xml(committee_block, :quorums, quorums)
47
+ ::Leap::Report.new self, quorum => conclusion
33
48
  end
34
49
  end
35
50
  end
36
51
 
37
52
  include ::Blockenspiel::DSL
53
+
54
+ # Defines a quorum within this committee.
55
+ #
56
+ # A quorum encapsulate a specific methodology for drawing the committe's conclusion.
57
+ # @param [String] name A descriptive name for this methodology.
58
+ # @param [optional, Hash] options
59
+ # @yield The quorum's methodology, as a Ruby closure. You may define your block with any number of block vars. If zero, the methodology requires no outside information (useful for "default" quorums). If one, Leap will provide the characteristics hash, accumulated over the course of the deliberation, that represents its total knowledge of the subject at the time. (Note that Leap will actually provide only a subset of the characteristics hash--namely, only attributes that are specifically requested by the <tt>:needs</tt> and <tt>:wants</tt> options below.) If more than one, additional block vars will be filled with as many considerations (given during <tt>Leap::Decision#make</tt>) as is necessary.
60
+ # @option [Symbol, Array] needs An attribute (or array of attributes) which must be known at the time of the committee's assembly for the quorum to be acknowledged.
61
+ # @option [Symbol, Array] wants An attribute (or array of attributes) which are useful to the methodology, but not required.
62
+ # @option complies A protocol (or array of protocols) with which this particular quorum complies. See <tt>Leap::GoalMethodsDocumentation</tt>.
38
63
  def quorum(name, options = {}, &blk)
39
64
  @quorums << ::Leap::Quorum.new(name, options, blk)
40
65
  end
41
66
 
67
+ # Convenience method for defining a quorum named "default" with no needs, wants, or compliances.
42
68
  def default(options = {}, &blk)
43
69
  quorum 'default', options, &blk
44
70
  end
@@ -1,5 +1,5 @@
1
1
  class Array
2
- # like detect, but returns the calculated value
2
+ # Like <tt>Array#detect</tt>, but returns the calculated value
3
3
  def grab(&blk)
4
4
  result = each do |element|
5
5
  value = yield element