ruleby 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,33 +14,24 @@ module Ruleby
14
14
  module Core
15
15
 
16
16
  class Atom
17
- attr_reader:name
18
- attr_reader:tag
19
- attr_reader:proc
20
- attr_reader:clazz
17
+ attr_reader :tag, :proc, :method, :deftemplate
21
18
 
22
- def initialize(tag, name, clazz, &block)
19
+ def initialize(tag, method, deftemplate, &block)
23
20
  @tag = tag
24
- @name = name
25
- @clazz = clazz
21
+ @method = method
22
+ @deftemplate = deftemplate
26
23
  @proc = Proc.new(&block) if block_given?
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
24
  end
34
25
 
35
26
  def to_s
36
- return "#{self.class}, tag=#{@tag}, name=#{@name}, class=#{@clazz}"
27
+ return "#{self.class},#{@tag},#{@method},#{@deftemplate}"
37
28
  end
38
29
  end
39
30
 
40
- # This kind of atom is used to match just a single, hard coded value.
31
+ # This kind of atom is used to match a simple condition.
41
32
  # For example:
42
33
  #
43
- # a.name = 'John'
34
+ # a.person{ |p| p.is_a? Person }
44
35
  #
45
36
  # So there are no references to other atoms.
46
37
  class PropertyAtom < Atom
@@ -49,36 +40,72 @@ module Ruleby
49
40
  end
50
41
 
51
42
  def shareable?(atom)
52
- return atom && @name == atom.name && @clazz = atom.clazz && @proc == atom.proc
43
+ return PropertyAtom === atom &&
44
+ @method == atom.method &&
45
+ @deftemplate == atom.deftemplate &&
46
+ @proc == atom.proc
47
+ end
48
+ end
49
+
50
+ # TODO use this
51
+ class BlockAtom < PropertyAtom
52
+ def shareable?(atom)
53
+ return super &&
54
+ BlockAtom === atom &&
55
+ @proc == atom.proc
56
+ end
57
+ end
58
+
59
+ # This kind of atom is used to match just a single, hard coded value.
60
+ # For example:
61
+ #
62
+ # a.name == 'John'
63
+ #
64
+ # So there are no references to other atoms.
65
+ class EqualsAtom < PropertyAtom
66
+ attr_reader :value
67
+ def initialize(tag, method, deftemplate, value)
68
+ super(tag,method,deftemplate)
69
+ @value = value
70
+ end
71
+
72
+ def shareable?(atom)
73
+ return EqualsAtom === atom &&
74
+ @method == atom.method &&
75
+ @deftemplate == atom.deftemplate
53
76
  end
54
77
  end
55
78
 
56
79
  # This kind of atom is used to match a class type. For example:
57
80
  #
58
- # p.has Person
81
+ # 'For each Person as :p'
59
82
  #
60
83
  # It is only used at the start of a pattern.
61
- class TypeAtom < Atom
62
- def initialize(tag, clazz)
63
- super tag, :class, clazz do |t| t == clazz end
84
+ class HeadAtom < Atom
85
+ def initialize(tag, deftemplate)
86
+ if deftemplate.mode == :equals
87
+ super tag, :class, deftemplate do |t| t == deftemplate.clazz end
88
+ elsif deftemplate.mode == :inherits
89
+ super tag, :class, deftemplate do |t| t === deftemplate.clazz end
90
+ end
64
91
  end
65
92
 
66
93
  def shareable?(atom)
67
- return atom && @clazz = atom.clazz
94
+ return HeadAtom === atom && @deftemplate == atom.deftemplate
68
95
  end
69
96
  end
70
97
 
71
98
  # This kind of atom is used for matching a value that is a variable.
72
99
  # For example:
73
- #
74
- # a.name = :your_name, :%
100
+ #
101
+ # #name == #:your_name
75
102
  #
76
103
  # The expression for this atom depends on some other atom.
77
104
  class ReferenceAtom < Atom
78
105
  attr_reader :vars
79
106
 
80
- def initialize(tag, name, vars, clazz, &block)
81
- super(tag, name, clazz, &block)
107
+ def initialize(tag, method, vars, deftemplate, &block)
108
+ super(tag, method, deftemplate, &block)
82
109
  @vars = vars # list of referenced variable names
83
110
  end
84
111
 
@@ -87,7 +114,11 @@ module Ruleby
87
114
  end
88
115
 
89
116
  def ==(atom)
90
- return atom.kind_of?(ReferenceAtom) && @proc == atom.proc && @tag == atom.tag && @vars == atom.vars
117
+ return ReferenceAtom === atom &&
118
+ @proc == atom.proc &&
119
+ @tag == atom.tag &&
120
+ @vars == atom.vars &&
121
+ @deftemplate == atom.deftemplate
91
122
  end
92
123
 
93
124
  def to_s
@@ -100,6 +131,24 @@ module Ruleby
100
131
  # *methods* that this atom references (not the variable names)!
101
132
  class SelfReferenceAtom < ReferenceAtom
102
133
  end
134
+
135
+ # This class encapsulates the criteria the HeadAtom uses to match. The clazz
136
+ # attribute represents a Class type, and the mode defines whether the head
137
+ # will match only class that are exactly a particular type, or if it will
138
+ # match classes that inherit that type also.
139
+ class DefTemplate
140
+ attr_reader :clazz
141
+ attr_reader :mode
142
+
143
+ def initialize(clazz,mode=:equals)
144
+ @clazz = clazz
145
+ @mode = mode
146
+ end
147
+
148
+ def ==(df)
149
+ DefTemplate === df && df.clazz == @clazz && df.mode == @mode
150
+ end
151
+ end
103
152
 
104
153
  end
105
154
  end
@@ -30,8 +30,8 @@ module Ruleby
30
30
  @priority = 0
31
31
  end
32
32
 
33
- def fire(r, match)
34
- @proc.call(r, match)
33
+ def fire(match)
34
+ @proc.call(match)
35
35
  end
36
36
 
37
37
  def ==(a2)
@@ -55,9 +55,9 @@ module Ruleby
55
55
  @used = false
56
56
  end
57
57
 
58
- def fire(r)
58
+ def fire()
59
59
  @used = true
60
- @action.fire r, @match
60
+ @action.fire @match
61
61
  end
62
62
 
63
63
  def <=>(a2)
@@ -135,6 +135,8 @@ module Ruleby
135
135
  # inference engine will compare these facts with the rules to produce some
136
136
  # outcomes.
137
137
  class WorkingMemory
138
+ attr_reader :facts
139
+
138
140
  def initialize
139
141
  @recency = 0
140
142
  @facts = Array.new
@@ -181,6 +183,10 @@ module Ruleby
181
183
  @conflict_resolver = cr
182
184
  @wm_altered = false
183
185
  end
186
+
187
+ def facts
188
+ @working_memory.facts.collect{|f| f.object}
189
+ end
184
190
 
185
191
  # This method id called to add a new fact to working memory
186
192
  def assert(object,&block)
@@ -217,7 +223,7 @@ module Ruleby
217
223
  agenda = @conflict_resolver.resolve agenda
218
224
  activation = agenda.pop
219
225
  used_agenda.push activation
220
- activation.fire self
226
+ activation.fire
221
227
  if @wm_altered
222
228
  agenda = @root.matches(false)
223
229
  @root.increment_counter
@@ -18,7 +18,8 @@ module Ruleby
18
18
  class RootNode
19
19
  def initialize(working_memory)
20
20
  @working_memory = working_memory
21
- @type_nodes = {}
21
+ @type_node = nil
22
+ @inherit_nodes = []
22
23
  @atom_nodes = []
23
24
  @join_nodes = []
24
25
  @terminal_nodes = []
@@ -37,14 +38,10 @@ module Ruleby
37
38
  # this method is called. It finds any nodes that depend on it, and updates
38
39
  # them accordingly.
39
40
  def assert_fact(fact)
40
- node = @type_nodes[fact.object.class]
41
- if node
42
- if fact.token == :plus
43
- node.assert(fact)
44
- else
45
- node.retract fact
46
- end
47
- end
41
+ @type_node and fact.token == :plus ? @type_node.assert(fact) : @type_node.retract(fact)
42
+ @inherit_nodes.each do |node|
43
+ fact.token == :plus ? node.assert(fact) : node.retract(fact)
44
+ end
48
45
  end
49
46
 
50
47
  # Increments the activation counter. This is just a pass-thru to the static
@@ -78,11 +75,15 @@ module Ruleby
78
75
  end
79
76
 
80
77
  def print
81
- puts 'NETWORK:'
82
- @terminal_nodes.each do |node|
83
- node.print(' ')
78
+ puts 'NETWORK:'
79
+ @terminal_nodes.each do |n|
80
+ n.print(' ')
84
81
  end
85
82
  end
83
+
84
+ def child_nodes
85
+ return @inherit_nodes + [@type_node]
86
+ end
86
87
 
87
88
  private
88
89
 
@@ -116,27 +117,31 @@ module Ruleby
116
117
  # side - if the out_node is a JoinNode, this marks the side
117
118
  def create_atom_nodes(pattern, out_node, side)
118
119
  # TODO refactor this method so it clear and concise
119
- type_node = create_type_node(pattern)
120
-
120
+ type_node = create_type_node(pattern)
121
+ forked = false
122
+ parent_atom = pattern.atoms[0]
121
123
  parent_node = type_node
122
- for i in (1..(pattern.atoms.size-1))
123
- node = nil
124
- atom = pattern.atoms[i]
124
+
125
+ pattern.atoms[1..-1].each do |atom|
126
+ # If the network has been forked, we don't want to share nodes anymore
127
+ forked = true if parent_node.forks?(parent_atom)
128
+
125
129
  if atom.kind_of?(SelfReferenceAtom)
126
130
  node = create_self_reference_node(atom)
127
131
  elsif atom.kind_of?(ReferenceAtom)
128
132
  node = create_reference_node(atom)
129
133
  out_node.ref_nodes.push node
130
134
  else
131
- node = create_property_node(atom)
135
+ node = create_property_node(atom,forked)
132
136
  end
133
- parent_node.out_nodes.push node
137
+ parent_node.add_out_node node, parent_atom
134
138
  node.parent_nodes.push parent_node
135
139
  parent_node = node
140
+ parent_atom = atom
136
141
  end
137
142
 
138
143
  bridge_node = create_bridge_node(pattern)
139
- parent_node.out_nodes.push bridge_node
144
+ parent_node.add_out_node bridge_node, parent_atom
140
145
  bridge_node.parent_nodes.push parent_node
141
146
  parent_node = bridge_node
142
147
 
@@ -144,11 +149,11 @@ module Ruleby
144
149
 
145
150
  if out_node.kind_of?(JoinNode)
146
151
  adapter_node = create_adapter_node(side)
147
- parent_node.out_nodes.push adapter_node
152
+ parent_node.add_out_node adapter_node
148
153
  parent_node = adapter_node
149
154
  end
150
155
 
151
- parent_node.out_nodes.push out_node
156
+ parent_node.add_out_node out_node
152
157
  compare_to_wm(type_node)
153
158
  return type_node
154
159
  end
@@ -172,29 +177,33 @@ module Ruleby
172
177
  parent_node = join_node
173
178
  if out_node.kind_of?(JoinNode)
174
179
  adapter_node = create_adapter_node(side)
175
- parent_node.out_nodes.push adapter_node
180
+ parent_node.add_out_node adapter_node
176
181
  parent_node = adapter_node
177
182
  end
178
- parent_node.out_nodes.push out_node
183
+ parent_node.add_out_node out_node
179
184
  return join_node
180
185
  end
181
186
 
182
187
  def create_type_node(pattern)
183
- atom = pattern.atoms[0]
184
- node = @type_nodes[atom.clazz]
185
- unless node
186
- node = TypeNode.new atom
187
- @type_nodes[atom.clazz] = node
188
+ if InheritsPattern === pattern
189
+ node = InheritsNode.new pattern.atoms[0]
190
+ @inherit_nodes.each do |inode|
191
+ return inode if inode.shareable? node
192
+ end
193
+ @inherit_nodes << node
194
+ return node
195
+ else
196
+ return (@type_node ||= TypeNode.new pattern.atoms[0])
188
197
  end
189
- return node
190
198
  end
191
199
 
192
200
  def create_bridge_node(pattern)
193
201
  return BridgeNode.new(pattern)
194
202
  end
195
203
 
196
- def create_property_node(atom)
197
- node = PropertyNode.new atom
204
+ def create_property_node(atom,forked)
205
+ node = atom.kind_of?(EqualsAtom) ? EqualsNode.new(atom) : PropertyNode.new(atom)
206
+ @atom_nodes.each {|n| return n if n.shareable? node} unless forked
198
207
  @atom_nodes.push node
199
208
  return node
200
209
  end
@@ -233,8 +242,7 @@ module Ruleby
233
242
  # Any node in the network that needs to be printed extends this class. It
234
243
  # provides handles to the nodes above it in the network. These are not used
235
244
  # for matching (i.e. no backward-chaining).
236
- class Printable
237
-
245
+ class Printable
238
246
  attr_reader:parent_nodes
239
247
 
240
248
  def initialize
@@ -252,8 +260,7 @@ module Ruleby
252
260
 
253
261
  # Base Node class used by all nodes in the network that do some kind
254
262
  # of matching.
255
- class Node < Printable
256
-
263
+ class Node < Printable
257
264
  # This method determines if all common tags have equal values. If any
258
265
  # values are not equal then the method returns false.
259
266
  def resolve(mr1, mr2)
@@ -271,21 +278,32 @@ module Ruleby
271
278
  # This is the base class for all nodes in the network that output to some
272
279
  # other node (i.e. they are not at the bottom). It contains methods for
273
280
  # propagating match results.
274
- class ParentNode < Node
275
-
276
- attr_reader:out_nodes
277
-
281
+ class ParentNode < Node
282
+ attr_reader :child_nodes
278
283
  def initialize()
279
284
  super
280
285
  @out_nodes = []
281
286
  end
282
287
 
288
+ def add_out_node(node,atom=nil)
289
+ unless @out_nodes.index node
290
+ @out_nodes.push node
291
+ end
292
+ end
293
+
294
+ # returns true if this node is already being used for the same atom. That
295
+ # is, if it is used again it will fork the network (or the network may
296
+ # already be forked).
297
+ def forks?(atom)
298
+ return !@out_nodes.empty?
299
+ end
300
+
283
301
  def retract(fact)
284
302
  propagate_retract(fact)
285
303
  end
286
304
 
287
- def propagate_retract(fact)
288
- @out_nodes.each do |out_node|
305
+ def propagate_retract(fact,out_nodes=@out_nodes)
306
+ out_nodes.each do |out_node|
289
307
  out_node.retract(fact)
290
308
  end
291
309
  end
@@ -294,8 +312,8 @@ module Ruleby
294
312
  propagate_assert(assertable)
295
313
  end
296
314
 
297
- def propagate_assert(assertable)
298
- @out_nodes.each do |out_node|
315
+ def propagate_assert(assertable,out_nodes=@out_nodes)
316
+ out_nodes.each do |out_node|
299
317
  out_node.assert(assertable)
300
318
  end
301
319
  end
@@ -304,49 +322,132 @@ module Ruleby
304
322
  # This is a base class for all single input nodes that match facts based on
305
323
  # some properties. It is essentially a wrapper for an Atom. These nodes make
306
324
  # up the Alpha network.
307
- class AtomNode < ParentNode
308
-
309
- attr_reader:atom
310
-
325
+ class AtomNode < ParentNode
326
+ attr_reader:atom
311
327
  def initialize(atom)
312
328
  super()
313
329
  @atom = atom
314
330
  end
331
+
332
+ def ==(node)
333
+ return AtomNode === node && @atom == node.atom
334
+ end
335
+
336
+ def shareable?(node)
337
+ return @atom.shareable?(node.atom)
338
+ end
339
+
340
+ def to_s
341
+ super + " - #{@atom.method}"
342
+ end
315
343
  end
316
344
 
317
- # This node class is used to match the type of a fact.
318
- class TypeNode < AtomNode
319
- def assert(fact)
320
- propagate_assert(fact) if (@atom.clazz == fact.object.class)
345
+ # This is a base class for any node that hashes out_nodes by value. A node
346
+ # that inherits this class does not evaluate each condition, instead it looks
347
+ # up the expected value in the hash, and gets a list of out_nodes.
348
+ class HashedNode < AtomNode
349
+ def initialize(atom)
350
+ super
351
+ @values = {}
352
+ @values.default = []
353
+ end
354
+
355
+ # returns true if this node is already being used for the same atom. That
356
+ # is, if it is used again it will fork the network (or the network may
357
+ # already be forked).
358
+ def forks?(atom)
359
+ k = hash_by(atom)
360
+ return !@values[k].empty?
361
+ end
362
+
363
+ def add_out_node(node,atom)
364
+ k = hash_by(atom)
365
+ v = @values[k]
366
+ if v.empty?
367
+ @values[k] = [node]
368
+ elsif !v.index node
369
+ @values[k] = v << node
370
+ end
371
+ end
372
+
373
+ def retract(fact)
374
+ propagate_retract fact, @values.values.flatten
375
+ end
376
+
377
+ def assert(fact)
378
+ k = fact.object.send(@atom.method)
379
+ propagate_assert fact, @values[k]
380
+ rescue NoMethodError => e
381
+ # If the method does not exist, it is the same as if it evaluted to
382
+ # false, and the network traverse stops
383
+ end
384
+ end
385
+
386
+ # This node class is used to match the type of a fact. In this case the type
387
+ # is matched exactly (ignoring inheritance).
388
+ class TypeNode < HashedNode
389
+ def hash_by(atom)
390
+ atom.deftemplate.clazz
391
+ end
392
+ end
393
+
394
+ # This class is used for the same purpose as the TypeNode, but it matches
395
+ # if the fact's inheritance chain includes the specified class.
396
+ class InheritsNode < TypeNode
397
+ def assert(fact)
398
+ @values.each do |clazz,nodes|
399
+ propagate_assert fact, nodes if clazz === fact.object
400
+ end
321
401
  end
322
402
  end
323
403
 
324
404
  # This node class is used for matching properties of a fact.
325
405
  class PropertyNode < AtomNode
326
406
  def assert(fact)
327
- val = fact.object.send("#{@atom.name}")
328
- propagate_assert(fact) if @atom.proc.call(val)
407
+ begin
408
+ val = fact.object.send(@atom.method)
409
+ rescue NoMethodError => e
410
+ # If the method does not exist, it is the same as if it evaluted to
411
+ # false, and the network traverse stops
412
+ return
413
+ end
414
+ super if @atom.proc.call(val)
329
415
  end
330
416
  end
331
417
 
418
+ # This node class is used for matching properties of a fact where the
419
+ # condition is a simple '=='. Instead of evaluating the condition, this node
420
+ # will pull from a hash. This makes it significatly fast when it is shared.
421
+ class EqualsNode < HashedNode
422
+ def hash_by(atom)
423
+ atom.value
424
+ end
425
+ end
426
+
332
427
  # This node class is used to match properties of one with the properties
333
428
  # of any other already matched fact. It differs from the other AtomNodes
334
429
  # because it does not perform any inline matching. The match method is only
335
430
  # invoked by the two input node.
336
431
  class ReferenceNode < AtomNode
337
432
  def match(left_context,right_fact)
338
- val = right_fact.object.send("#{@atom.name}")
433
+ val = right_fact.object.send(@atom.method)
339
434
  args = [val]
340
435
  match = left_context.match
341
436
  @atom.vars.each do |var|
342
437
  args.push match.variables[var]
343
- end
344
- if @atom.proc.call(*args)
345
- m = MatchResult.new(match.variables.clone, true, match.fact_hash.clone, match.recency)
346
- m.recency.push right_fact.recency
347
- m.fact_hash[@atom.tag] = right_fact.id
348
- m.variables[@atom.tag] = val
349
- return m
438
+ end
439
+ begin
440
+ if @atom.proc.call(*args)
441
+ m = MatchResult.new(match.variables.clone, true,
442
+ match.fact_hash.clone, match.recency)
443
+ m.recency.push right_fact.recency
444
+ m.fact_hash[@atom.tag] = right_fact.id
445
+ m.variables[@atom.tag] = val
446
+ return m
447
+ end
448
+ rescue NoMethodError => e
449
+ # If the method does not exist, it is the same as if it evaluted to
450
+ # false, and the network traverse stops
350
451
  end
351
452
  return MatchResult.new
352
453
  end
@@ -360,7 +461,7 @@ module Ruleby
360
461
  end
361
462
 
362
463
  def match(fact)
363
- args = [fact.object.send("#{@atom.name}")]
464
+ args = [fact.object.send(@atom.method)]
364
465
  @atom.vars.each do |var|
365
466
  args.push fact.object.send(var)
366
467
  end
@@ -389,13 +490,10 @@ module Ruleby
389
490
  # HACK its a pain to have to check for this, can we make it special
390
491
  mr[atom.tag] = fact.object
391
492
  else
392
- mr[atom.tag] = fact.object.send("#{atom.name}")
493
+ mr[atom.tag] = fact.object.send(atom.method)
393
494
  end
394
495
  end
395
-
396
- context = MatchContext.new(fact,mr)
397
-
398
- super(context)
496
+ super(MatchContext.new(fact,mr))
399
497
  end
400
498
  end
401
499
 
@@ -561,9 +659,11 @@ module Ruleby
561
659
  end
562
660
  end
563
661
 
564
- def assert_left(context)
662
+ def assert_left(context)
565
663
  add_to_left_memory(context)
566
- unless @ref_nodes.empty? && @right_memory.empty?
664
+ if @ref_nodes.empty? && @right_memory.empty?
665
+ propagate_assert(context)
666
+ else
567
667
  propagate = true
568
668
  @right_memory.values.each do |right_context|
569
669
  if match_ref_nodes(context,right_context)
@@ -577,7 +677,11 @@ module Ruleby
577
677
 
578
678
  def assert_right(context)
579
679
  @right_memory[context.fact.id] = context
580
- unless @ref_nodes.empty?
680
+ if @ref_nodes.empty?
681
+ @left_memory.values.flatten.each do |left_context|
682
+ propagate_retract_resolve(left_context.match)
683
+ end
684
+ else
581
685
  @left_memory.values.flatten.each do |left_context|
582
686
  if match_ref_nodes(left_context,context)
583
687
  # QUESTION is there a more efficient way to retract here?