ruleby 0.2 → 0.3

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.
@@ -1,6 +1,18 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner, Matt Smith
10
+ #
11
+
1
12
  $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
2
13
  require 'ruleby'
3
- require 'rulebook'
14
+
15
+ include Ruleby
4
16
 
5
17
  class Message
6
18
  def initialize(status,message)
@@ -12,22 +24,52 @@ class Message
12
24
  end
13
25
 
14
26
  class SelfRefRulebook < Rulebook
15
- def rules
16
- rule 'HelloWorld' do |r|
27
+ def rules
28
+ rule :LeTigreTest,
29
+ 'For each Message as :m where #message as :x #&& #status == #:x' do |r,v|
30
+ puts 'Success'
31
+ end
32
+
33
+ rule :LeTigreTest,
34
+ [Message, :m, {m.message => :x}, m.status == b(:x)] do |r,v|
35
+ puts 'Success'
36
+ end
37
+
38
+ rule :BlueSteelTest do |r|
17
39
  r.when do |has|
18
- has.m2 Message
19
- has.m2.message :x
20
- has.m2.status = :x, :%
40
+ has.m Message
41
+ has.m.message :x
42
+ has.m.status = :x, :%
21
43
  end
22
- r.then = action do |e,vars|
44
+ r.then do |e,vars|
23
45
  puts 'Success'
24
46
  end
25
47
  end
48
+
49
+ # NOTE references the self class binding is not allowed yet
50
+
51
+ # rule 'LeTigreTest',
52
+ # 'exists? Message as :m where #status == #:m.message' do |r,v|
53
+ # puts 'Success'
54
+ # end
55
+
56
+ # rule 'LeTigreTest',
57
+ # [Message, :m, m.status(:m, &c{|s,m| s == m.message})] do |r,v|
58
+ # puts 'Success'
59
+ # end
60
+
61
+ # rule :BlueSteelTest do |r|
62
+ # r.when do |has|
63
+ # has.m Message
64
+ # has.m.status :m do |s,m| s == m.message end
65
+ # end
66
+ # r.then do |e,vars|
67
+ # puts 'Success'
68
+ # end
69
+ # end
26
70
  end
27
71
  end
28
72
 
29
- include Ruleby
30
-
31
73
  engine :engine do |e|
32
74
  SelfRefRulebook.new(e).rules
33
75
  e.assert Message.new(:HELLO, :HELLO)
@@ -1,27 +1,46 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner
10
+ #
11
+
1
12
  #tokens
2
13
  module Ruleby
3
14
  module Core
4
15
 
5
- # TODO eventually we want to refactor some of the match? and
6
- # match_result logic out of these classes and into an AtomNode
7
- # class. This will allow the Atoms to simply contain some attrs
8
-
9
- class Atom
10
- def initialize(tag, name, &block)
16
+ class Atom
17
+ attr_reader:name
18
+ attr_reader:tag
19
+ attr_reader:proc
20
+ attr_reader:clazz
21
+
22
+ def initialize(tag, name, clazz, &block)
11
23
  @tag = tag
12
24
  @name = name
25
+ @clazz = clazz
13
26
  @proc = Proc.new(&block) if block_given?
14
- end
27
+
28
+ # QUESTION we should probably change the '@clazz' variable to be called
29
+ # something else. Like '@deftemplate' maybe?
30
+
31
+ # QUESTION we should probably change the '@name' variable to be called
32
+ # something else. Like '@method' maybe?
33
+ end
15
34
 
16
- attr_reader :name
17
- attr_reader :tag
18
- attr_reader :proc
35
+ def to_s
36
+ return "#{self.class}, tag=#{@tag}, name=#{@name}, class=#{@clazz}"
37
+ end
19
38
  end
20
39
 
21
40
  # This kind of atom is used to match just a single, hard coded value.
22
41
  # For example:
23
42
  #
24
- # a.name do |n| n == 'John' end
43
+ # a.name = 'John'
25
44
  #
26
45
  # So there are no references to other atoms.
27
46
  class PropertyAtom < Atom
@@ -30,11 +49,7 @@ module Ruleby
30
49
  end
31
50
 
32
51
  def shareable?(atom)
33
- return atom && @name == atom.name && @proc == atom.proc
34
- end
35
-
36
- def to_s
37
- return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
52
+ return atom && @name == atom.name && @clazz = atom.clazz && @proc == atom.proc
38
53
  end
39
54
  end
40
55
 
@@ -43,126 +58,48 @@ module Ruleby
43
58
  # p.has Person
44
59
  #
45
60
  # It is only used at the start of a pattern.
46
- class TypeAtom < PropertyAtom
47
-
61
+ class TypeAtom < Atom
62
+ def initialize(tag, clazz)
63
+ super tag, :class, clazz do |t| t == clazz end
64
+ end
65
+
66
+ def shareable?(atom)
67
+ return atom && @clazz = atom.clazz
68
+ end
48
69
  end
49
70
 
50
71
  # This kind of atom is used for matching a value that is a variable.
51
72
  # For example:
52
73
  #
53
- # a.name references => :your_name do |n,yn| n == yn end
74
+ # a.name = :your_name, :%
54
75
  #
55
76
  # The expression for this atom depends on some other atom.
56
- class ReferenceAtom < Atom
57
-
58
- def initialize(tag, name, vars, &block)
59
- super(tag, name, &block)
77
+ class ReferenceAtom < Atom
78
+ attr_reader :vars
79
+
80
+ def initialize(tag, name, vars, clazz, &block)
81
+ super(tag, name, clazz, &block)
60
82
  @vars = vars # list of referenced variable names
61
- end
83
+ end
62
84
 
63
- attr_reader :vars
85
+ def shareable?(atom)
86
+ false
87
+ end
64
88
 
65
89
  def ==(atom)
66
90
  return atom.kind_of?(ReferenceAtom) && @proc == atom.proc && @tag == atom.tag && @vars == atom.vars
67
91
  end
68
92
 
69
93
  def to_s
70
- return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
94
+ return super + ", vars=#{vars.join(',')}"
71
95
  end
72
96
  end
73
97
 
74
98
  # This is an atom that references another atom that is in the same pattern.
99
+ # Note that in a SelfReferenceAtom, the 'vars' argument must be a list of the
100
+ # *methods* that this atom references (not the variable names)!
75
101
  class SelfReferenceAtom < ReferenceAtom
76
-
77
- end
78
-
79
- class MatchResult
80
-
81
- # TODO this class needs to be cleaned up for that we don't have a bunch of
82
- # properties. Instead, maybe it sould have a list of facts.
83
- def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
84
- @variables = variables
85
-
86
- # a list of recencies of the facts that this matchresult depends on.
87
- @recency = recency
88
-
89
- # notes where this match result is from a NotPattern or ObjectPattern
90
- # TODO this isn't really needed anymore. how can we get rid of it?
91
- @is_match = is_match
92
-
93
- # a hash of fact.ids that each tag corresponds to
94
- # QUESTION what is better: this or the @backward_hash in MatchContext?
95
- # Right now these two are somewhat redundate.
96
- @fact_hash = fact_hash
97
- end
98
- def []=(sym, object)
99
- @variables[sym] = object
100
- end
101
- def [](sym)
102
- return @variables[sym]
103
- end
104
- def fact_ids
105
- return fact_hash.values.uniq
106
- end
107
- attr :variables, true
108
- attr :is_match, true
109
- attr :fact_hash, true
110
- attr :resolved, true
111
- attr :recency, true
112
- def ==(match)
113
- return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
114
- end
115
-
116
- def key?(m)
117
- return @variables.key?(m)
118
- end
119
-
120
- def keys
121
- return @variables.keys
122
- end
123
-
124
- def update(mr)
125
- @recency = @recency | mr.recency
126
- @is_match = mr.is_match
127
- @variables = @variables.update mr.variables
128
-
129
- # QUESTION why the heck does this statement work instead of the
130
- # commented one below it??
131
- @fact_hash = mr.fact_hash.update @fact_hash
132
- #@fact_hash = @fact_hash.update mr.fact_hash
133
-
134
- return self
135
- end
136
-
137
- def dup
138
- dup_mr = MatchResult.new
139
- dup_mr.recency = @recency.clone
140
- dup_mr.is_match = @is_match
141
- dup_mr.variables = @variables.clone
142
- dup_mr.fact_hash = @fact_hash.clone
143
- return dup_mr
144
- end
145
-
146
- def clear
147
- @variables = {}
148
- @fact_hash = {}
149
- @recency = []
150
- end
151
-
152
- def delete(tag)
153
- @variables.delete(tag)
154
- @fact_hash.delete(tag)
155
- end
156
-
157
- def to_s
158
- s = '#MatchResult('
159
- s = s + 'f)(' unless @is_match
160
- s = s + object_id.to_s+')('
161
- @variables.each do |key,value|
162
- s += "#{key}=#{value}/#{@fact_hash[key]}, "
163
- end
164
- return s + ")"
165
- end
166
102
  end
103
+
167
104
  end
168
105
  end
@@ -1,4 +1,14 @@
1
- #core core
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner, Matt Smith
10
+ #
11
+
2
12
  require 'core/atoms'
3
13
  require 'core/patterns'
4
14
  require 'core/utils'
@@ -6,17 +16,20 @@ require 'core/nodes'
6
16
 
7
17
  module Ruleby
8
18
  module Core
9
- class Action
19
+
20
+ # An action is a wrapper for a code block that will be executed if a rule is
21
+ # satisfied.
22
+ class Action
23
+ attr_accessor :priority
24
+ attr_accessor :name
25
+ attr_reader :matches
26
+
10
27
  def initialize(&block)
11
28
  @name = nil
12
29
  @proc = Proc.new(&block) if block_given?
13
30
  @priority = 0
14
31
  end
15
-
16
- attr_accessor :priority
17
- attr_accessor :name
18
- attr_reader :matches
19
-
32
+
20
33
  def fire(r, match)
21
34
  @proc.call(r, match)
22
35
  end
@@ -26,19 +39,24 @@ module Ruleby
26
39
  end
27
40
  end
28
41
 
29
- class Activation
30
- def initialize(action, match)
42
+ # An activation is an action/match pair that is executed if a rule is matched.
43
+ # It also contains metadata that can be used for conflict resolution if two
44
+ # rules are satisfied by the same fact.
45
+ class Activation
46
+ attr_reader :action, :match
47
+ attr_accessor :counter, :used
48
+
49
+ def initialize(action, match, counter=0)
31
50
  @action = action
32
51
  @match = match
33
52
  @match.recency.sort!
34
53
  @match.recency.reverse!
35
- @counter = 0
36
- end
37
-
38
- attr_reader :action, :match
39
- attr_accessor :counter
54
+ @counter = counter
55
+ @used = false
56
+ end
40
57
 
41
58
  def fire(r)
59
+ @used = true
42
60
  @action.fire r, @match
43
61
  end
44
62
 
@@ -62,41 +80,16 @@ module Ruleby
62
80
  end
63
81
  end
64
82
 
65
- class Rule
83
+ class Rule
84
+ attr_accessor :pattern
85
+ attr_reader :action, :name, :priority
86
+
66
87
  def initialize(name, pattern=nil, action=nil, priority=0)
67
88
  @name = name
68
89
  @pattern = pattern
69
90
  @action = action
70
91
  @priority = priority
71
92
  end
72
-
73
- attr_accessor:pattern
74
- attr_reader:action, :name
75
-
76
-
77
- def when(&block)
78
- wb = WhenBuilder.new
79
- yield wb
80
- @pattern = wb.pattern
81
- end
82
-
83
- def then(&block)
84
- @action = Core::Action.new(&block)
85
- end
86
-
87
- def when=(pattern)
88
- @pattern = pattern
89
- end
90
-
91
- def then=(action)
92
- @action = action
93
- @action.name = @name
94
- @action.priority = @priority
95
- end
96
-
97
- def priority
98
- return @priority
99
- end
100
93
 
101
94
  def priority=(p)
102
95
  @priority = p
@@ -104,16 +97,18 @@ module Ruleby
104
97
  end
105
98
  end
106
99
 
107
- class Fact
108
- def initialize(object, token)
109
- @token = token
110
- @object = object
111
- end
112
-
100
+ # A fact is an object that is stored in working memory. The rules in the
101
+ # system will either look for the existence or absence of particular facts.
102
+ class Fact
113
103
  attr :token, true
114
104
  attr :recency, true
115
105
  attr_reader :object
116
106
 
107
+ def initialize(object, token)
108
+ @token = token
109
+ @object = object
110
+ end
111
+
117
112
  def id
118
113
  return object.object_id
119
114
  end
@@ -127,12 +122,18 @@ module Ruleby
127
122
  end
128
123
  end
129
124
 
130
- class RulebyConflictResolver
125
+ # A conflict resolver is used to order activations that become active at the
126
+ # same time. The default implementation sorts the agenda based on the
127
+ # properties of the activation.
128
+ class RulebyConflictResolver
131
129
  def resolve(agenda)
132
130
  return agenda.sort
133
- end
131
+ end
134
132
  end
135
133
 
134
+ # The working memory is a container for all the facts in the system. The
135
+ # inference engine will compare these facts with the rules to produce some
136
+ # outcomes.
136
137
  class WorkingMemory
137
138
  def initialize
138
139
  @recency = 0
@@ -170,65 +171,59 @@ module Ruleby
170
171
  end
171
172
  end
172
173
 
174
+ # This is the core class of the library. A new rule engine is create by
175
+ # instantiating it. Each rule engine has one inference engine, one rule set
176
+ # and one working memory.
173
177
  class Engine
174
- def initialize(wm=WorkingMemory.new)
178
+ def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new)
175
179
  @root = nil
176
180
  @working_memory = wm
177
- @conflict_resolver = RulebyConflictResolver.new
181
+ @conflict_resolver = cr
182
+ @wm_altered = false
178
183
  end
179
- #assert fact
184
+
185
+ # This method id called to add a new fact to working memory
180
186
  def assert(object,&block)
187
+ @wm_altered = true
181
188
  fact_helper(object,:plus,&block)
182
189
  end
183
- #retract fact
190
+
191
+ # This method is called to remove an existing fact from working memory
184
192
  def retract(object,&block)
193
+ @wm_altered = true
185
194
  fact_helper(object,:minus,&block)
186
195
  end
187
- #modify fact
188
- # retract
189
- # assert
196
+
197
+ # This method is called to alter an existing fact. It is essentially a
198
+ # retract followed by an assert.
190
199
  def modify(object,&block)
191
200
  retract(object,&block)
192
201
  assert(object,&block)
193
202
  end
194
203
 
195
- def assert_fact(fact)
196
- wm_fact = @working_memory.assert_fact fact
197
- @root.assert_fact wm_fact if @root != nil
198
- end
199
-
200
- def assert_rule(rule)
204
+ # This method adds a new rule to the system.
205
+ def assert_rule(rule)
201
206
  @root = RootNode.new(@working_memory) if @root == nil
202
207
  @root.assert_rule rule
203
208
  end
204
209
 
205
- def match(agenda=@root.match, used_agenda=[], activation_counter=0)
206
- while (agenda.length > 0)
207
- agenda = @conflict_resolver.resolve agenda
208
- activation = agenda.pop
209
- used_agenda.push activation
210
- activation.fire self
211
-
212
- new_agenda = @root.match
213
-
214
- # HACK the following is a workaround. This problem would best be
215
- # solved by working this into the nodes themselves.
216
- new_agenda.each do |a|
217
- used = false
218
- used_agenda.each do |used_activation|
219
- used = true if used_activation.object_id == a.object_id
210
+ # This method executes the activations that were generated by the rules
211
+ # that match facts in working memory.
212
+ def match(agenda=nil, used_agenda=[])
213
+ if @root
214
+ @root.reset_counter
215
+ agenda = @root.matches unless agenda
216
+ while (agenda.length > 0)
217
+ agenda = @conflict_resolver.resolve agenda
218
+ activation = agenda.pop
219
+ used_agenda.push activation
220
+ activation.fire self
221
+ if @wm_altered
222
+ agenda = @root.matches(false)
223
+ @root.increment_counter
224
+ @wm_altered = false
220
225
  end
221
-
222
- # BUG we are comparing against the current agenda, but we may need to
223
- # compare against all activations that have existed...
224
- if (agenda.index(a) == nil) && (!used)
225
- a.counter = activation_counter+1
226
- agenda.push a
227
- end
228
- end
229
- agenda.delete_if {|a| new_agenda.index(a) == nil}
230
-
231
- activation_counter = activation_counter+1
226
+ end
232
227
  end
233
228
  end
234
229
 
@@ -238,12 +233,17 @@ module Ruleby
238
233
  end
239
234
 
240
235
  private
241
- def fact_helper(object, sign=:plus, &block)
242
- f = Core::Fact.new object, sign
243
- yield f if block_given?
244
- assert_fact f
245
- f
246
- end
236
+ def fact_helper(object, sign=:plus, &block)
237
+ f = Core::Fact.new object, sign
238
+ yield f if block_given?
239
+ assert_fact f
240
+ f
241
+ end
242
+
243
+ def assert_fact(fact)
244
+ wm_fact = @working_memory.assert_fact fact
245
+ @root.assert_fact wm_fact if @root != nil
246
+ end
247
247
  end
248
248
  end
249
249
  end