ruleby 0.7 → 0.8.b1

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