ruleby 0.7 → 0.8.b1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/core/atoms.rb CHANGED
@@ -136,7 +136,7 @@ module Ruleby
136
136
  # attribute represents a Class type, and the mode defines whether the head
137
137
  # will match only class that are exactly a particular type, or if it will
138
138
  # match classes that inherit that type also.
139
- class DefTemplate
139
+ class Template
140
140
  attr_reader :clazz
141
141
  attr_reader :mode
142
142
 
@@ -146,7 +146,7 @@ module Ruleby
146
146
  end
147
147
 
148
148
  def ==(df)
149
- DefTemplate === df && df.clazz == @clazz && df.mode == @mode
149
+ Template === df && df.clazz == @clazz && df.mode == @mode
150
150
  end
151
151
  end
152
152
 
data/lib/core/engine.rb CHANGED
@@ -1,269 +1,273 @@
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
-
12
- require 'core/atoms'
13
- require 'core/patterns'
14
- require 'core/utils'
15
- require 'core/nodes'
16
-
17
- module Ruleby
18
- module Core
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
- attr_reader :proc
27
-
28
- def initialize(&block)
29
- @name = nil
30
- @proc = Proc.new(&block) if block_given?
31
- @priority = 0
32
- end
33
-
34
- def fire(match, engine=nil)
35
- if @proc.arity == 2
36
- @proc.call(match, engine)
37
- else
38
- @proc.call(match)
39
- end
40
- end
41
-
42
- def ==(a2)
43
- return @name == a2.name
44
- end
45
- end
46
-
47
- # An activation is an action/match pair that is executed if a rule is matched.
48
- # It also contains metadata that can be used for conflict resolution if two
49
- # rules are satisfied by the same fact.
50
- class Activation
51
- attr_reader :action, :match
52
- attr_accessor :counter, :used
53
-
54
- def initialize(action, match, counter=0)
55
- @action = action
56
- @match = match
57
- @match.recency.sort!
58
- @match.recency.reverse!
59
- @counter = counter
60
- @used = false
61
- end
62
-
63
- def fire(engine=nil)
64
- @used = true
65
- @action.fire @match, engine
66
- end
67
-
68
- def <=>(a2)
69
- return @counter <=> a2.counter if @counter != a2.counter
70
- return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority
71
-
72
- # NOTE in order for this to work, the array must be reverse sorted
73
- i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
74
- i += 1
75
- end
76
- return @match.recency[i] <=> a2.match.recency[i]
77
- end
78
-
79
- def ==(a2)
80
- return a2 != nil && @action == a2.action && @match == a2.match
81
- end
82
-
83
- def to_s
84
- return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] "
85
- end
86
- end
87
-
88
- class Rule
89
- attr_accessor :pattern
90
- attr_reader :action, :name, :priority
91
-
92
- def initialize(name, pattern=nil, action=nil, priority=0)
93
- @name = name
94
- @pattern = pattern
95
- @action = action
96
- @priority = priority
97
- end
98
-
99
- def priority=(p)
100
- @priority = p
101
- @action.priority = @priority
102
- end
103
- end
104
-
105
- # A fact is an object that is stored in working memory. The rules in the
106
- # system will either look for the existence or absence of particular facts.
107
- class Fact
108
- attr :token, true
109
- attr :recency, true
110
- attr_reader :object
111
-
112
- def initialize(object, token)
113
- @token = token
114
- @object = object
115
- end
116
-
117
- def id
118
- return object.object_id
119
- end
120
-
121
- def ==(fact)
122
- return fact != nil && fact.id == id
123
- end
124
-
125
- def to_s
126
- return "[Fact |#{@recency}|#{@object.to_s}]"
127
- end
128
- end
129
-
130
- # A conflict resolver is used to order activations that become active at the
131
- # same time. The default implementation sorts the agenda based on the
132
- # properties of the activation.
133
- class RulebyConflictResolver
134
- def resolve(agenda)
135
- return agenda.sort
136
- end
137
- end
138
-
139
- # The working memory is a container for all the facts in the system. The
140
- # inference engine will compare these facts with the rules to produce some
141
- # outcomes.
142
- class WorkingMemory
143
- attr_reader :facts
144
-
145
- def initialize
146
- @recency = 0
147
- @facts = Array.new
148
- end
149
-
150
- def each_fact
151
- @facts.each do |f|
152
- yield(f)
153
- end
154
- end
155
-
156
- def assert_fact(fact)
157
- raise 'The fact asserted cannot be nil!' unless fact.object
158
- if (fact.token == :plus)
159
- fact.recency = @recency
160
- @recency += 1
161
- @facts.push fact
162
- return fact
163
- else #if (fact.token == :minus)
164
- i = @facts.index(fact)
165
- raise 'The fact to remove does not exist!' unless i
166
- existing_fact = @facts[i]
167
- @facts.delete_at(i)
168
- existing_fact.token = fact.token
169
- return existing_fact
170
- end
171
- end
172
-
173
- def print
174
- puts 'WORKING MEMORY:'
175
- @facts.each do |fact|
176
- puts " #{fact.object} - #{fact.id} - #{fact.recency}"
177
- end
178
- end
179
- end
180
-
181
- # This is the core class of the library. A new rule engine is created by
182
- # instantiating it. Each rule engine has one inference engine, one rule set
183
- # and one working memory.
184
- class Engine
185
-
186
- def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new)
187
- @root = nil
188
- @working_memory = wm
189
- @conflict_resolver = cr
190
- @wm_altered = false
191
- assert InitialFact.new
192
- end
193
-
194
- def facts
195
- @working_memory.facts.collect{|f| f.object}
196
- end
197
-
198
- # This method id called to add a new fact to working memory
199
- def assert(object,&block)
200
- @wm_altered = true
201
- fact_helper(object,:plus,&block)
202
- end
203
-
204
- # This method is called to remove an existing fact from working memory
205
- def retract(object,&block)
206
- @wm_altered = true
207
- fact_helper(object,:minus,&block)
208
- end
209
-
210
- # This method is called to alter an existing fact. It is essentially a
211
- # retract followed by an assert.
212
- def modify(object,&block)
213
- retract(object,&block)
214
- assert(object,&block)
215
- end
216
-
217
- def retrieve(c)
218
- facts.select {|f| f.kind_of?(c)}
219
- end
220
-
221
- # This method adds a new rule to the system.
222
- def assert_rule(rule)
223
- if @root == nil
224
- @root = RootNode.new(@working_memory)
225
- @root.reset_counter
226
- end
227
- @root.assert_rule rule
228
- end
229
-
230
- # This method executes the activations that were generated by the rules
231
- # that match facts in working memory.
232
- def match(agenda=nil, used_agenda=[])
233
- if @root
234
- @root.reset_counter
235
- agenda = @root.matches unless agenda
236
- while (agenda.length > 0)
237
- agenda = @conflict_resolver.resolve agenda
238
- activation = agenda.pop
239
- used_agenda.push activation
240
- activation.fire self
241
- if @wm_altered
242
- agenda = @root.matches(false)
243
- @root.increment_counter
244
- @wm_altered = false
245
- end
246
- end
247
- end
248
- end
249
-
250
- def print
251
- @working_memory.print
252
- @root.print
253
- end
254
-
255
- private
256
- def fact_helper(object, sign=:plus, &block)
257
- f = Core::Fact.new object, sign
258
- yield f if block_given?
259
- assert_fact f
260
- f
261
- end
262
-
263
- def assert_fact(fact)
264
- wm_fact = @working_memory.assert_fact fact
265
- @root.assert_fact wm_fact if @root != nil
266
- end
267
- end
268
- end
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
+
12
+ require 'core/atoms'
13
+ require 'core/patterns'
14
+ require 'core/utils'
15
+ require 'core/nodes'
16
+
17
+ module Ruleby
18
+ module Core
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
+ attr_reader :proc
27
+
28
+ def initialize(&block)
29
+ @name = nil
30
+ @proc = Proc.new(&block) if block_given?
31
+ @priority = 0
32
+ end
33
+
34
+ def fire(match, engine=nil)
35
+ if @proc.arity == 2
36
+ @proc.call(match, engine)
37
+ else
38
+ @proc.call(match)
39
+ end
40
+ end
41
+
42
+ def ==(a2)
43
+ return @name == a2.name
44
+ end
45
+ end
46
+
47
+ # An activation is an action/match pair that is executed if a rule is matched.
48
+ # It also contains metadata that can be used for conflict resolution if two
49
+ # rules are satisfied by the same fact.
50
+ class Activation
51
+ attr_reader :action, :match
52
+ attr_accessor :counter, :used
53
+
54
+ def initialize(action, match, counter=0)
55
+ @action = action
56
+ @match = match
57
+ @match.recency.sort!
58
+ @match.recency.reverse!
59
+ @counter = counter
60
+ @used = false
61
+ end
62
+
63
+ def fire(engine=nil)
64
+ @used = true
65
+ @action.fire @match, engine
66
+ end
67
+
68
+ def <=>(a2)
69
+ return @counter <=> a2.counter if @counter != a2.counter
70
+ return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority
71
+
72
+ # NOTE in order for this to work, the array must be reverse sorted
73
+ i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
74
+ i += 1
75
+ end
76
+ return @match.recency[i] <=> a2.match.recency[i]
77
+ end
78
+
79
+ def ==(a2)
80
+ return a2 != nil && @action == a2.action && @match == a2.match
81
+ end
82
+
83
+ def to_s
84
+ return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] "
85
+ end
86
+ end
87
+
88
+ class Rule
89
+ attr_accessor :pattern
90
+ attr_reader :action, :name, :priority
91
+
92
+ def initialize(name, pattern=nil, action=nil, priority=0)
93
+ @name = name
94
+ @pattern = pattern
95
+ @action = action
96
+ @priority = priority
97
+ end
98
+
99
+ def priority=(p)
100
+ @priority = p
101
+ @action.priority = @priority
102
+ end
103
+ end
104
+
105
+ # A fact is an object that is stored in working memory. The rules in the
106
+ # system will either look for the existence or absence of particular facts.
107
+ class Fact
108
+ attr :token, true
109
+ attr :recency, true
110
+ attr_reader :object
111
+
112
+ def initialize(object, token)
113
+ @token = token
114
+ @object = object
115
+ end
116
+
117
+ def id
118
+ return object.object_id
119
+ end
120
+
121
+ def ==(fact)
122
+ if fact.is_a? Fact
123
+ fact != nil && fact.id == id
124
+ else
125
+ fact != nil && fact.object_id == id
126
+ end
127
+ end
128
+
129
+ def to_s
130
+ "[Fact |#{@recency}|#{@object.to_s}]"
131
+ end
132
+ end
133
+
134
+ # A conflict resolver is used to order activations that become active at the
135
+ # same time. The default implementation sorts the agenda based on the
136
+ # properties of the activation.
137
+ class RulebyConflictResolver
138
+ def resolve(agenda)
139
+ return agenda.sort
140
+ end
141
+ end
142
+
143
+ # The working memory is a container for all the facts in the system. The
144
+ # inference engine will compare these facts with the rules to produce some
145
+ # outcomes.
146
+ class WorkingMemory
147
+ attr_reader :facts
148
+
149
+ def initialize
150
+ @recency = 0
151
+ @facts = Array.new
152
+ end
153
+
154
+ def each_fact
155
+ @facts.each do |f|
156
+ yield(f)
157
+ end
158
+ end
159
+
160
+ def assert_fact(fact)
161
+ raise 'The fact asserted cannot be nil!' unless fact.object
162
+ if (fact.token == :plus)
163
+ fact.recency = @recency
164
+ @recency += 1
165
+ @facts.push fact
166
+ return fact
167
+ else #if (fact.token == :minus)
168
+ i = @facts.index(fact)
169
+ raise 'The fact to remove does not exist!' unless i
170
+ existing_fact = @facts[i]
171
+ @facts.delete_at(i)
172
+ existing_fact.token = fact.token
173
+ return existing_fact
174
+ end
175
+ end
176
+
177
+ def print
178
+ puts 'WORKING MEMORY:'
179
+ @facts.each do |fact|
180
+ puts " #{fact.object} - #{fact.id} - #{fact.recency}"
181
+ end
182
+ end
183
+ end
184
+
185
+ # This is the core class of the library. A new rule engine is created by
186
+ # instantiating it. Each rule engine has one inference engine, one rule set
187
+ # and one working memory.
188
+ class Engine
189
+
190
+ def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new)
191
+ @root = nil
192
+ @working_memory = wm
193
+ @conflict_resolver = cr
194
+ @wm_altered = false
195
+ assert InitialFact.new
196
+ end
197
+
198
+ def facts
199
+ @working_memory.facts.collect{|f| f.object}
200
+ end
201
+
202
+ # This method id called to add a new fact to working memory
203
+ def assert(object,&block)
204
+ @wm_altered = true
205
+ fact_helper(object,:plus,&block)
206
+ end
207
+
208
+ # This method is called to remove an existing fact from working memory
209
+ def retract(object,&block)
210
+ @wm_altered = true
211
+ fact_helper(object,:minus,&block)
212
+ end
213
+
214
+ # This method is called to alter an existing fact. It is essentially a
215
+ # retract followed by an assert.
216
+ def modify(object,&block)
217
+ retract(object,&block)
218
+ assert(object,&block)
219
+ end
220
+
221
+ def retrieve(c)
222
+ facts.select {|f| f.kind_of?(c)}
223
+ end
224
+
225
+ # This method adds a new rule to the system.
226
+ def assert_rule(rule)
227
+ if @root == nil
228
+ @root = RootNode.new(@working_memory)
229
+ @root.reset_counter
230
+ end
231
+ @root.assert_rule rule
232
+ end
233
+
234
+ # This method executes the activations that were generated by the rules
235
+ # that match facts in working memory.
236
+ def match(agenda=nil, used_agenda=[])
237
+ if @root
238
+ @root.reset_counter
239
+ agenda = @root.matches unless agenda
240
+ while (agenda.length > 0)
241
+ agenda = @conflict_resolver.resolve agenda
242
+ activation = agenda.pop
243
+ used_agenda.push activation
244
+ activation.fire self
245
+ if @wm_altered
246
+ agenda = @root.matches(false)
247
+ @root.increment_counter
248
+ @wm_altered = false
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ def print
255
+ @working_memory.print
256
+ @root.print
257
+ end
258
+
259
+ private
260
+ def fact_helper(object, sign=:plus, &block)
261
+ f = Core::Fact.new object, sign
262
+ yield f if block_given?
263
+ assert_fact f
264
+ f
265
+ end
266
+
267
+ def assert_fact(fact)
268
+ wm_fact = @working_memory.assert_fact fact
269
+ @root.assert_fact wm_fact if @root != nil
270
+ end
271
+ end
272
+ end
269
273
  end
data/lib/core/nodes.rb CHANGED
@@ -200,7 +200,11 @@ module Ruleby
200
200
  end
201
201
 
202
202
  def create_bridge_node(pattern)
203
- return BridgeNode.new(pattern)
203
+ if pattern.kind_of?(CollectPattern)
204
+ CollectNode.new(pattern)
205
+ else
206
+ BridgeNode.new(pattern)
207
+ end
204
208
  end
205
209
 
206
210
  def create_property_node(atom,forked)
@@ -483,12 +487,14 @@ module Ruleby
483
487
  # network, or to the terminal nodes. It creates a partial match from the
484
488
  # pattern and atoms above it in the network. Thus, there is one bridge node
485
489
  # for each pattern (assuming they aren't shared).
486
- class BridgeNode < ParentNode
490
+ class BaseBridgeNode < ParentNode
487
491
  def initialize(pattern)
488
492
  super()
489
493
  @pattern = pattern
490
494
  end
491
-
495
+ end
496
+
497
+ class BridgeNode < BaseBridgeNode
492
498
  def propagate_assert(fact)
493
499
  # create the partial match
494
500
  mr = MatchResult.new
@@ -505,7 +511,72 @@ module Ruleby
505
511
  end
506
512
  super(MatchContext.new(fact,mr))
507
513
  end
508
- end
514
+ end
515
+
516
+ class CollectNode < BaseBridgeNode
517
+ def initialize(pattern)
518
+ super
519
+ @collection_memory = Fact.new([], :internal)
520
+ @should_modify = false
521
+ # not really sure what to do about this. might just need to handle nil's
522
+ # using a puts a limit on how many facts can be asserted before this feature breaks
523
+ # it might be best to get a handle to WorkingMemory in order to get the real
524
+ # @collection_memory.recency = 0
525
+ end
526
+
527
+ def retract(fact)
528
+ propagate_retract(@collection_memory)
529
+ propagate_assert(fact) do
530
+ @collection_memory.object.delete_if {|a| a == fact.object}
531
+ @should_modify = false
532
+ end
533
+ end
534
+
535
+ def propagate_modify(context, out_nodes=@out_nodes)
536
+ out_nodes.each do |out_node|
537
+ if out_node.is_a?(TerminalNode)
538
+ out_node.modify(context)
539
+ else
540
+ raise "You can't join to :collect patterns yet!"
541
+ end
542
+ end
543
+ end
544
+
545
+ def propagate_assert(fact)
546
+ if block_given?
547
+ yield fact
548
+ else
549
+ @collection_memory.object << fact
550
+ @collection_memory.recency = fact.recency
551
+ end
552
+
553
+ if @should_modify
554
+ propagate_modify(MatchContext.new(@collection_memory, create_match_result))
555
+ else
556
+ if @collection_memory.object.size > 0
557
+ @should_modify = true
558
+ super(MatchContext.new(@collection_memory, create_match_result))
559
+ end
560
+ end
561
+ end
562
+
563
+ private
564
+
565
+ def create_match_result()
566
+ # create the partial match
567
+ mr = MatchResult.new
568
+ mr.is_match = true
569
+ mr.recency.push @collection_memory.recency
570
+
571
+ @pattern.atoms.each do |atom|
572
+ mr.fact_hash[atom.tag] = @collection_memory.id
573
+ if atom == @pattern.head
574
+ mr[atom.tag] = @collection_memory.object
575
+ end
576
+ end
577
+ mr
578
+ end
579
+ end
509
580
 
510
581
  # This class is used to plug nodes into the left input of a two-input JoinNode
511
582
  class LeftAdapterNode < ParentNode
@@ -549,7 +620,7 @@ module Ruleby
549
620
  end
550
621
  end
551
622
  end
552
-
623
+
553
624
  # This class is a two-input node that is used to create a cross-product of the
554
625
  # two network branches above it. It keeps a memory of the left and right
555
626
  # inputs and compares new facts to each. These nodes make up what is called
@@ -740,13 +811,30 @@ module Ruleby
740
811
  @rule = rule
741
812
  @activations = MultiHash.new
742
813
  end
743
- attr_reader:activations
744
-
814
+ #attr_reader:activations
815
+
816
+ def activations
817
+ @activations
818
+ end
819
+
745
820
  def assert(context)
746
821
  match = context.match
747
822
  a = Activation.new(@rule.action, match, @@counter)
748
823
  @activations.add match.fact_ids, a
749
824
  end
825
+
826
+ def modify(context)
827
+ found = false
828
+ @activations.each do |id, v|
829
+ if context.match.fact_ids.sort == id.sort
830
+ v.replace(context.fact)
831
+ found = true
832
+ end
833
+ end
834
+ if !found
835
+ assert(context)
836
+ end
837
+ end
750
838
 
751
839
  def retract(fact)
752
840
  @activations.remove fact.id
data/lib/core/patterns.rb CHANGED
@@ -1,116 +1,119 @@
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
-
12
- module Ruleby
13
- module Core
14
-
15
- class Pattern
16
- end
17
-
18
- # This class represents a pattern that is looking for the existence of some
19
- # object. It contains a list of 'atoms' that represent the properties of
20
- # the class that we are looking for.
21
- class ObjectPattern < Pattern
22
- attr_reader :atoms
23
-
24
- def initialize(head, atoms)
25
- @atoms = [head] + atoms
26
- end
27
-
28
- def head
29
- @atoms[0]
30
- end
31
-
32
- def ==(pattern)
33
- if pattern.class == self.class
34
- atoms = pattern.atoms
35
- if(@atoms.size == atoms.size)
36
- (0..@atoms.size).each do |i|
37
- if !(@atoms[i] == atoms[i])
38
- return false
39
- end
40
- end
41
- return true
42
- end
43
- end
44
- return false
45
- end
46
-
47
- def to_s
48
- return '(' + @atoms.join('|') + ')'
49
- end
50
- end
51
-
52
- class InheritsPattern < ObjectPattern
53
- end
54
-
55
- # This class represents a pattern that is looking for the absence of some
56
- # object (rather than the existence of). In all respects, it is the same as
57
- # an ObjectPattern, but it is handled differently by the inference engine.
58
- class NotPattern < ObjectPattern
59
- end
60
-
61
- class NotInheritsPattern < InheritsPattern
62
- end
63
-
64
- # A composite pattern represents a logical conjunction of two patterns. The
65
- # inference engine interprets this differently from an ObjectPattern because
66
- # it simply aggregates patterns.
67
- class CompositePattern < Pattern
68
-
69
- attr_reader :left_pattern
70
- attr_reader :right_pattern
71
-
72
- def initialize(left_pattern, right_pattern)
73
- @left_pattern = left_pattern
74
- @right_pattern = right_pattern
75
- end
76
-
77
- def atoms
78
- atoms = []
79
- atoms.push @left_pattern.atoms
80
- atoms.push @right_pattern.atoms
81
- return atoms
82
- end
83
- end
84
-
85
- class AndPattern < CompositePattern
86
-
87
- def initialize(left_pattern, right_pattern)
88
- super(left_pattern, right_pattern)
89
- @head = :and
90
- end
91
-
92
- end
93
-
94
- class OrPattern < CompositePattern
95
-
96
- def initialize(left_pattern, right_pattern)
97
- super(left_pattern, right_pattern)
98
- @head = :or
99
- end
100
-
101
- end
102
-
103
- class InitialFactPattern < ObjectPattern
104
- def initialize
105
- deftemplate = DefTemplate.new InitialFact, :equals
106
- htag = GeneratedTag.new
107
- head = HeadAtom.new htag, deftemplate
108
- super(head, [])
109
- end
110
- end
111
-
112
- class PatternFactory
113
- # TODO add some convenience methods for creating patterns
114
- end
115
- end
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
+
12
+ module Ruleby
13
+ module Core
14
+
15
+ class Pattern
16
+ end
17
+
18
+ # This class represents a pattern that is looking for the existence of some
19
+ # object. It contains a list of 'atoms' that represent the properties of
20
+ # the class that we are looking for.
21
+ class ObjectPattern < Pattern
22
+ attr_reader :atoms
23
+
24
+ def initialize(head, atoms)
25
+ @atoms = [head] + atoms
26
+ end
27
+
28
+ def head
29
+ @atoms[0]
30
+ end
31
+
32
+ def ==(pattern)
33
+ if pattern.class == self.class
34
+ atoms = pattern.atoms
35
+ if(@atoms.size == atoms.size)
36
+ (0..@atoms.size).each do |i|
37
+ if !(@atoms[i] == atoms[i])
38
+ return false
39
+ end
40
+ end
41
+ return true
42
+ end
43
+ end
44
+ return false
45
+ end
46
+
47
+ def to_s
48
+ return '(' + @atoms.join('|') + ')'
49
+ end
50
+ end
51
+
52
+ class InheritsPattern < ObjectPattern
53
+ end
54
+
55
+ class CollectPattern < ObjectPattern
56
+ end
57
+
58
+ # This class represents a pattern that is looking for the absence of some
59
+ # object (rather than the existence of). In all respects, it is the same as
60
+ # an ObjectPattern, but it is handled differently by the inference engine.
61
+ class NotPattern < ObjectPattern
62
+ end
63
+
64
+ class NotInheritsPattern < InheritsPattern
65
+ end
66
+
67
+ # A composite pattern represents a logical conjunction of two patterns. The
68
+ # inference engine interprets this differently from an ObjectPattern because
69
+ # it simply aggregates patterns.
70
+ class CompositePattern < Pattern
71
+
72
+ attr_reader :left_pattern
73
+ attr_reader :right_pattern
74
+
75
+ def initialize(left_pattern, right_pattern)
76
+ @left_pattern = left_pattern
77
+ @right_pattern = right_pattern
78
+ end
79
+
80
+ def atoms
81
+ atoms = []
82
+ atoms.push @left_pattern.atoms
83
+ atoms.push @right_pattern.atoms
84
+ return atoms
85
+ end
86
+ end
87
+
88
+ class AndPattern < CompositePattern
89
+
90
+ def initialize(left_pattern, right_pattern)
91
+ super(left_pattern, right_pattern)
92
+ @head = :and
93
+ end
94
+
95
+ end
96
+
97
+ class OrPattern < CompositePattern
98
+
99
+ def initialize(left_pattern, right_pattern)
100
+ super(left_pattern, right_pattern)
101
+ @head = :or
102
+ end
103
+
104
+ end
105
+
106
+ class InitialFactPattern < ObjectPattern
107
+ def initialize
108
+ deftemplate = Template.new InitialFact, :equals
109
+ htag = GeneratedTag.new
110
+ head = HeadAtom.new htag, deftemplate
111
+ super(head, [])
112
+ end
113
+ end
114
+
115
+ class PatternFactory
116
+ # TODO add some convenience methods for creating patterns
117
+ end
118
+ end
116
119
  end
data/lib/dsl/ferrari.rb CHANGED
@@ -216,12 +216,15 @@ module Ruleby
216
216
  def when(*args)
217
217
  clazz = AtomBuilder === args[0] ? nil : args.shift
218
218
  is_not = false
219
+ is_collect = false
219
220
  mode = :equals
220
221
  while clazz.is_a? Symbol
221
222
  if clazz == :not || clazz == :~
222
223
  is_not = true
223
224
  elsif clazz == :is_a? || clazz == :kind_of? || clazz == :instance_of?
224
225
  mode = :inherits
226
+ elsif clazz == :collect
227
+ is_collect = true
225
228
  elsif clazz == :exists?
226
229
  raise 'The \'exists\' quantifier is not yet supported.'
227
230
  end
@@ -233,7 +236,7 @@ module Ruleby
233
236
  mode = :inherits
234
237
  end
235
238
 
236
- deftemplate = Core::DefTemplate.new clazz, mode
239
+ deftemplate = Core::Template.new clazz, mode
237
240
  atoms = []
238
241
  @when_counter += 1
239
242
  htag = Symbol === args[0] ? args.shift : GeneratedTag.new
@@ -259,14 +262,15 @@ module Ruleby
259
262
  else
260
263
  raise "Invalid condition: #{arg}"
261
264
  end
262
- end
265
+ end
263
266
 
264
267
  if is_not
265
268
  p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) :
266
269
  Core::NotPattern.new(head, atoms)
267
270
  else
268
271
  p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) :
269
- Core::ObjectPattern.new(head, atoms)
272
+ is_collect ? Core::CollectPattern.new(head, atoms) :
273
+ Core::ObjectPattern.new(head, atoms)
270
274
  end
271
275
  @pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
272
276
  end
@@ -293,6 +297,8 @@ module Ruleby
293
297
 
294
298
  class MethodBuilder
295
299
  public_instance_methods.each do |m|
300
+ # maybe we shouldn't be undefing object_id. What are the implications? Can we make object_id a
301
+ # pass through to the underlying object's object_id?
296
302
  a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
297
303
  undef_method m.to_sym unless a.include? m.to_sym
298
304
  end
@@ -318,31 +324,31 @@ module Ruleby
318
324
  @tag = tag
319
325
  @method = method
320
326
  end
321
-
327
+
322
328
  def +(arg)
323
329
  raise 'Cannot use operators in short-hand mode!'
324
330
  end
325
-
331
+
326
332
  def -(arg)
327
333
  raise 'Cannot use operators in short-hand mode!'
328
334
  end
329
-
335
+
330
336
  def /(arg)
331
337
  raise 'Cannot use operators in short-hand mode!'
332
338
  end
333
-
339
+
334
340
  def *(arg)
335
341
  raise 'Cannot use operators in short-hand mode!'
336
342
  end
337
-
343
+
338
344
  def to_s
339
345
  "BindingBuilder @tag=#{@tag}, @method=#{@method}"
340
346
  end
341
347
  end
342
-
348
+
343
349
  class AtomBuilder
344
350
  attr_accessor :tag, :name, :bindings, :deftemplate, :block
345
-
351
+
346
352
  def initialize(method_id)
347
353
  @name = method_id
348
354
  @deftemplate = nil
@@ -351,39 +357,48 @@ module Ruleby
351
357
  @block = lambda {|x| true}
352
358
  @child_atom_builders = []
353
359
  end
354
-
360
+
355
361
  def method_missing(method_id, *args, &block)
356
362
  if method_id == :not
357
363
  return NotOperatorBuilder.new(@name)
358
364
  end
359
365
  end
360
-
366
+
361
367
  def ==(value)
362
368
  @atom_type = :equals
363
369
  @value = value
364
- create_block value, lambda {|x,y| x == y}, lambda {|x| x == value}; self
370
+ create_block value, lambda {|x,y| convert(x, y) == y}, lambda {|x| convert(x, value) == value}; self
365
371
  end
366
-
372
+
367
373
  def >(value)
368
- create_block value, lambda {|x,y| x > y}, lambda {|x| x > value}; self
374
+ create_block value, lambda {|x,y| x != nil && convert(x, y) > y}, lambda {|x| x != nil && convert(x, value) > value}; self
369
375
  end
370
-
376
+
371
377
  def <(value)
372
- create_block value, lambda {|x,y| x < y}, lambda {|x| x < value}; self
378
+ create_block value, lambda {|x,y| x != nil && convert(x, y) < y}, lambda {|x| x != nil && convert(x, value) < value}; self
373
379
  end
374
-
380
+
375
381
  def =~(value)
376
- create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
382
+ create_block value, lambda {|x,y| convert(x, y) =~ y}, lambda {|x| convert(x, value) =~ value}; self
377
383
  end
378
-
384
+
379
385
  def <=(value)
380
- create_block value, lambda {|x,y| x <= y}, lambda {|x| x <= value}; self
386
+ create_block value, lambda {|x,y| x != nil && convert(x, y) <= y}, lambda {|x| x != nil && convert(x, value) <= value}; self
381
387
  end
382
-
388
+
383
389
  def >=(value)
384
- create_block value, lambda {|x,y| x >= y}, lambda {|x| x >= value}; self
390
+ create_block value, lambda {|x,y| x != nil && convert(x, y) >= y}, lambda {|x| x != nil && convert(x, value) >= value}; self
385
391
  end
386
-
392
+
393
+ def convert(x, value)
394
+ if x.nil?
395
+ return x
396
+ end
397
+ return x.to_f if value.kind_of? Float
398
+ return x.to_i if value.kind_of? Numeric
399
+ return x
400
+ end
401
+
387
402
  def build_atoms(tags,methods,when_id)
388
403
  atoms = @child_atom_builders.map { |atom_builder|
389
404
  tags[atom_builder.tag] = when_id
@@ -398,7 +413,7 @@ module Ruleby
398
413
  return atoms << Core::PropertyAtom.new(@tag, @name, @deftemplate, &@block)
399
414
  end
400
415
  end
401
-
416
+
402
417
  if references_self?(tags,when_id)
403
418
  bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
404
419
  atoms << Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,&@block)
@@ -407,7 +422,7 @@ module Ruleby
407
422
  atoms << Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,&@block)
408
423
  end
409
424
  end
410
-
425
+
411
426
  private
412
427
  def references_self?(tags,when_id)
413
428
  ref_self = 0
@@ -416,14 +431,14 @@ module Ruleby
416
431
  ref_self += 1
417
432
  end
418
433
  end
419
-
434
+
420
435
  if ref_self > 0 and ref_self != @bindings.size
421
436
  raise 'Binding to self and another pattern in the same condition is not yet supported.'
422
437
  end
423
-
438
+
424
439
  return ref_self > 0
425
440
  end
426
-
441
+
427
442
  def create_block(value, ref_block, basic_block)
428
443
  if value && value.kind_of?(BindingBuilder)
429
444
  @bindings = [value]
@@ -437,7 +452,7 @@ module Ruleby
437
452
  end
438
453
  end
439
454
  end
440
-
455
+
441
456
  class NotOperatorBuilder < AtomBuilder
442
457
  def ==(value)
443
458
  create_block value, lambda {|x,y| x != y}, lambda {|x| x != value}; self
data/lib/dsl/letigre.rb CHANGED
@@ -136,7 +136,7 @@ module Ruleby
136
136
  mode = :inherits
137
137
  end
138
138
 
139
- deftemplate = Core::DefTemplate.new clazz, mode
139
+ deftemplate = Core::Template.new clazz, mode
140
140
  head = Core::HeadAtom.new tag, deftemplate
141
141
 
142
142
  atoms = []
metadata CHANGED
@@ -1,16 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruleby
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.7"
4
+ prerelease: true
5
+ segments:
6
+ - 0
7
+ - 8
8
+ - b1
9
+ version: 0.8.b1
5
10
  platform: ruby
6
11
  authors:
7
- - Joe Kutner
8
- - Matt Smith
12
+ - Joe Kutner
13
+ - Matt Smith
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
17
 
13
- date: 2010-08-27 00:00:00 -05:00
18
+ date: 2011-03-29 00:00:00 -05:00
14
19
  default_executable:
15
20
  dependencies: []
16
21
 
@@ -28,17 +33,17 @@ extensions: []
28
33
  extra_rdoc_files: []
29
34
 
30
35
  files:
31
- - lib/core/atoms.rb
32
- - lib/core/engine.rb
33
- - lib/core/nodes.rb
34
- - lib/core/patterns.rb
35
- - lib/core/utils.rb
36
- - lib/dsl/ferrari.rb
37
- - lib/dsl/letigre.rb
38
- - lib/dsl/steel.rb
39
- - lib/rule_helper.rb
40
- - lib/rulebook.rb
41
- - lib/ruleby.rb
36
+ - lib/rule_helper.rb
37
+ - lib/rulebook.rb
38
+ - lib/ruleby.rb
39
+ - lib/core/atoms.rb
40
+ - lib/core/engine.rb
41
+ - lib/core/nodes.rb
42
+ - lib/core/patterns.rb
43
+ - lib/core/utils.rb
44
+ - lib/dsl/ferrari.rb
45
+ - lib/dsl/letigre.rb
46
+ - lib/dsl/steel.rb
42
47
  has_rdoc: true
43
48
  homepage: http://ruleby.org
44
49
  licenses: []
@@ -47,25 +52,31 @@ post_install_message:
47
52
  rdoc_options: []
48
53
 
49
54
  require_paths:
50
- - lib
55
+ - lib
51
56
  required_ruby_version: !ruby/object:Gem::Requirement
52
57
  requirements:
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- version: 1.8.2
56
- version:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 1
62
+ - 8
63
+ - 2
64
+ version: 1.8.2
57
65
  required_rubygems_version: !ruby/object:Gem::Requirement
58
66
  requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: "0"
62
- version:
67
+ - - ">"
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 1
71
+ - 3
72
+ - 1
73
+ version: 1.3.1
63
74
  requirements: []
64
75
 
65
76
  rubyforge_project: ruleby
66
- rubygems_version: 1.3.5
77
+ rubygems_version: 1.3.6
67
78
  signing_key:
68
79
  specification_version: 3
69
80
  summary: Rete based Ruby Rule Engine
70
81
  test_files:
71
- - tests/test.rb
82
+ - tests/test.rb