ruleby 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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