ruleby 0.3 → 0.4

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.
@@ -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?