ruleby 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,28 +1,37 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner
10
+ #
11
+
1
12
  module Ruleby
2
13
  module Core
3
14
 
4
15
  class Pattern
5
- # a list of AtomTag objects
6
- attr_reader :atom_tags
7
16
  end
8
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.
9
21
  class ObjectPattern < Pattern
10
-
11
- # 'head' is a class type
12
- def initialize(tag, head, atoms)
13
- init_vars tag, head, atoms
14
- end
15
22
 
16
- def init_vars(tag,head,atoms,match=true)
17
- @head = head
18
- @headatom = TypeAtom.new tag, :class do |t| t == head end
19
- @atoms = [@headatom] + atoms
20
- @atom_tags = @atoms.collect {|a| AtomTag.new a.tag, match}
21
- end
22
-
23
23
  attr_reader :head
24
24
  attr_reader :atoms
25
- attr_reader :atom_tags
25
+
26
+ # 'deftemplate' is a class type
27
+ def initialize(tag, deftemplate, atoms)
28
+ init_vars tag, deftemplate, atoms
29
+ end
30
+
31
+ def init_vars(tag,deftemplate,atoms)
32
+ @head = TypeAtom.new tag, deftemplate
33
+ @atoms = [@head] + atoms
34
+ end
26
35
 
27
36
  def ==(pattern)
28
37
  atoms = pattern.atoms
@@ -42,24 +51,27 @@ module Ruleby
42
51
  end
43
52
  end
44
53
 
54
+ # This class represents a pattern that is looking for the absence of some
55
+ # object (rather than the existence of). In all respects, it is the same as
56
+ # an ObjectPattern, but it is handled differently by the inference engine.
45
57
  class NotPattern < ObjectPattern
46
- def initialize(tag, head, atoms)
47
- init_vars tag, head, atoms, false
48
- end
49
-
50
58
  def ==(pattern)
51
59
  return pattern.kind_of?(NotPattern) && super==(pattern)
52
60
  end
53
61
  end
54
62
 
63
+ # A composite pattern represents a logical conjunction of two patterns. The
64
+ # inference engine interprets this differently from an ObjectPattern because
65
+ # it simply aggregates patterns.
55
66
  class CompositePattern < Pattern
67
+
68
+ attr_reader :left_pattern
69
+ attr_reader :right_pattern
56
70
 
57
71
  def initialize(left_pattern, right_pattern)
58
72
  @left_pattern = left_pattern
59
73
  @right_pattern = right_pattern
60
- end
61
-
62
- attr_reader :left_pattern, :right_pattern
74
+ end
63
75
 
64
76
  def atoms
65
77
  atoms = []
@@ -73,8 +85,7 @@ module Ruleby
73
85
 
74
86
  def initialize(left_pattern, right_pattern)
75
87
  super(left_pattern, right_pattern)
76
- @head = :and
77
- @atom_tags = left_pattern.atom_tags.concat(right_pattern.atom_tags)
88
+ @head = :and
78
89
  end
79
90
 
80
91
  end
@@ -84,24 +95,10 @@ module Ruleby
84
95
  def initialize(left_pattern, right_pattern)
85
96
  super(left_pattern, right_pattern)
86
97
  @head = :or
87
- @atom_tags = [[left_pattern.atom_tags, right_pattern.atom_tags]]
88
98
  end
89
99
 
90
100
  end
91
-
92
- class AtomTag
93
- def initialize(tag, exists=true)
94
- @tag = tag
95
-
96
- # QUESTION can we get rid of this property? if so, we may not need this
97
- # class at all.
98
- @exists = exists
99
- end
100
- attr_reader :tag,:exists
101
- def to_s
102
- return "[#{@tag.to_s}]"
103
- end
104
- end
101
+
105
102
 
106
103
  end
107
104
  end
@@ -1,9 +1,145 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner
10
+ #
11
+
1
12
  module Ruleby
2
13
  module Core
3
14
 
4
- # This class is used when we need to have a Hash where keys and values are mapped
5
- # many-to-many. This class allows for quick access of both key and value. It
6
- # is similar to Multimap in C++ standard lib.
15
+ # This class is a wrapper for the context under which the network executes for
16
+ # for a given fact. It is essentially a wrapper for a fact and a partial
17
+ # match.
18
+ class MatchContext
19
+
20
+ attr_reader:fact
21
+ attr_reader:match
22
+
23
+ def initialize(fact,mr)
24
+ @fact = fact
25
+ @match = mr
26
+ end
27
+
28
+ def to_s
29
+ return @match.to_s
30
+ end
31
+
32
+ def ==(t)
33
+ return t && @fact == t.fact && @match == t.match
34
+ end
35
+ end
36
+
37
+ # This class represents a partial match. It contains the variables, values,
38
+ # and some metadata about the match. For the most part, this metadata is used
39
+ # during conflict resolution.
40
+ class MatchResult
41
+ # TODO this class needs to be cleaned up so that we don't have a bunch of
42
+ # properties. Instead, maybe it sould have a list of facts.
43
+
44
+ attr :variables, true
45
+ attr :is_match, true
46
+ attr :fact_hash, true
47
+ attr :recency, true
48
+
49
+ def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
50
+ @variables = variables
51
+
52
+ # a list of recencies of the facts that this matchresult depends on.
53
+ @recency = recency
54
+
55
+ # notes where this match result is from a NotPattern or ObjectPattern
56
+ # TODO this isn't really needed anymore. how can we get rid of it?
57
+ @is_match = is_match
58
+
59
+ # a hash of fact.ids that each tag corresponds to
60
+ @fact_hash = fact_hash
61
+ end
62
+
63
+ def []=(sym, object)
64
+ @variables[sym] = object
65
+ end
66
+
67
+ def [](sym)
68
+ return @variables[sym]
69
+ end
70
+
71
+ def fact_ids
72
+ return fact_hash.values.uniq
73
+ end
74
+
75
+ def ==(match)
76
+ return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
77
+ end
78
+
79
+ def key?(m)
80
+ return @variables.key?(m)
81
+ end
82
+
83
+ def keys
84
+ return @variables.keys
85
+ end
86
+
87
+ def update(mr)
88
+ @recency = @recency | mr.recency
89
+ @is_match = mr.is_match
90
+ @variables = @variables.update mr.variables
91
+ @fact_hash = @fact_hash.update mr.fact_hash
92
+ return self
93
+ end
94
+
95
+ def dup
96
+ dup_mr = MatchResult.new
97
+ dup_mr.recency = @recency.clone
98
+ dup_mr.is_match = @is_match
99
+ dup_mr.variables = @variables.clone
100
+ dup_mr.fact_hash = @fact_hash.clone
101
+ return dup_mr
102
+ end
103
+
104
+ def merge!(mr)
105
+ return update(mr)
106
+ end
107
+
108
+ def merge(mr)
109
+ new_mr = MatchResult.new
110
+ new_mr.recency = @recency | mr.recency
111
+ new_mr.is_match = mr.is_match
112
+ new_mr.variables = @variables.merge mr.variables
113
+ new_mr.fact_hash = @fact_hash.merge mr.fact_hash
114
+ return new_mr
115
+ end
116
+
117
+ def clear
118
+ @variables = {}
119
+ @fact_hash = {}
120
+ @recency = []
121
+ end
122
+
123
+ def delete(tag)
124
+ @variables.delete(tag)
125
+ @fact_hash.delete(tag)
126
+ end
127
+
128
+ def to_s
129
+ s = '#MatchResult('
130
+ s = s + 'f)(' unless @is_match
131
+ s = s + object_id.to_s+')('
132
+ @variables.each do |key,value|
133
+ s += "#{key}=#{value}/#{@fact_hash[key]}, "
134
+ end
135
+ return s + ")"
136
+ end
137
+ end
138
+
139
+ # This class is used when we need to have a Hash where keys and values are
140
+ # mapped many-to-many. This class allows for quick access of both key and
141
+ # value. It is similar to Multimap in C++ standard lib.
142
+ # This thing is a mess (and barely works). It needs to be refactored.
7
143
  class MultiHash
8
144
  def initialize(key=nil, values=[])
9
145
  @i = 0
@@ -87,6 +223,7 @@ module Ruleby
87
223
  @backward_hash[xref] = ids
88
224
  end
89
225
 
226
+ # DEPRECATED
90
227
  # WARN this method adds a value to the MultiHash only if it is unique. It
91
228
  # can be a fairly costly operation, and should be avoided. We only
92
229
  # implemented this as part of a hack to get things working early on.
@@ -150,6 +287,7 @@ module Ruleby
150
287
  end
151
288
  end
152
289
 
290
+ # DEPRECATED
153
291
  # WARN see comments in add_uniq
154
292
  def concat_uniq(double_hash)
155
293
  double_hash.each do |ids,val|
@@ -0,0 +1,263 @@
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 Ferrari
14
+ class RulebookHelper
15
+ def initialize(engine)
16
+ @engine = engine
17
+ end
18
+
19
+ attr_reader :engine
20
+
21
+ def rule(name, *args, &block)
22
+ options = args[0].kind_of?(Hash) ? args.shift : {}
23
+
24
+ r = RuleBuilder.new name
25
+ args.each do |arg|
26
+ if arg.kind_of? Array
27
+ r.when(*arg)
28
+ else
29
+ raise 'Invalid condition. All or none must be Arrays.'
30
+ end
31
+ end
32
+
33
+ r.then(&block)
34
+ r.priority = options[:priority] if options[:priority]
35
+
36
+ @engine.assert_rule(r.build_rule)
37
+ end
38
+
39
+ end
40
+
41
+ class RuleBuilder
42
+
43
+ def initialize(name, pattern=nil, action=nil, priority=0)
44
+ @name = name
45
+ @pattern = pattern
46
+ @action = action
47
+ @priority = priority
48
+
49
+ @tags = {}
50
+ @methods = {}
51
+ @when_counter = 0
52
+ end
53
+
54
+ def when(*args)
55
+ clazz = args.shift
56
+ is_not = false
57
+ if clazz == :not || clazz == :~
58
+ clazz = args.shift
59
+ is_not = true
60
+ elsif clazz == :exists
61
+ raise 'The \'exists\' quantifier is not yet supported.'
62
+ end
63
+
64
+ head_tag = nil
65
+ atoms = []
66
+ @when_counter += 1
67
+
68
+ if args[0].kind_of?(Symbol)
69
+ head_tag = args.shift
70
+ @tags[head_tag] = @when_counter
71
+ else
72
+ head_tag = GeneratedTag.new
73
+ @tags[head_tag] = @when_counter
74
+ end
75
+
76
+ args.each do |arg|
77
+ if arg.kind_of? Hash
78
+ arg.each do |ab,tag|
79
+ ab.tag = tag
80
+ ab.clazz = clazz
81
+ @tags[tag] = @when_counter
82
+ @methods[tag] = ab.name
83
+ atoms.push ab.build_atom(@tags, @methods, @when_counter)
84
+ end
85
+ elsif arg.kind_of? AtomBuilder
86
+ arg.tag = GeneratedTag.new
87
+ arg.clazz = clazz
88
+ @methods[arg.tag] = arg.name
89
+ atoms.push arg.build_atom(@tags, @methods, @when_counter)
90
+ elsif arg == false
91
+ raise 'The != operator is not allowed.'
92
+ else
93
+ raise "Invalid condition: #{arg}"
94
+ end
95
+ end
96
+
97
+ p = is_not ? Core::NotPattern.new(head_tag, clazz, atoms) :
98
+ Core::ObjectPattern.new(head_tag, clazz, atoms)
99
+ @pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
100
+
101
+ return nil
102
+ end
103
+
104
+ def then(&block)
105
+ @action = Core::Action.new(&block)
106
+ @action.name = @name
107
+ @action.priority = @priority
108
+ end
109
+
110
+ def priority
111
+ return @priority
112
+ end
113
+
114
+ def priority=(p)
115
+ @priority = p
116
+ @action.priority = @priority
117
+ end
118
+
119
+ def build_rule
120
+ Core::Rule.new @name, @pattern, @action, @priority
121
+ end
122
+ end
123
+
124
+ class MethodBuilder
125
+ public_instance_methods.each do |m|
126
+ a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
127
+ undef_method m.to_sym unless a.include? m.to_sym
128
+ end
129
+
130
+ def method_missing(method_id, *args, &block)
131
+ ab = AtomBuilder.new method_id
132
+ if block_given?
133
+ args.each do |arg|
134
+ ab.bindings.push BindingBuilder.new(arg, method_id)
135
+ end
136
+ ab.block = block
137
+ elsif args.size > 0
138
+ puts args.class.to_s + ' --- ' + args.to_s
139
+ raise 'Arguments not supported for short-hand conditions'
140
+ end
141
+ return ab
142
+ end
143
+ end
144
+
145
+ class BindingBuilder
146
+ attr_accessor :tag, :method
147
+ def initialize(tag,method=nil)
148
+ @tag = tag
149
+ @method = method
150
+ end
151
+
152
+ def +(arg)
153
+ raise 'Cannot use operators in short-hand mode!'
154
+ end
155
+
156
+ def -(arg)
157
+ raise 'Cannot use operators in short-hand mode!'
158
+ end
159
+
160
+ def /(arg)
161
+ raise 'Cannot use operators in short-hand mode!'
162
+ end
163
+
164
+ def *(arg)
165
+ raise 'Cannot use operators in short-hand mode!'
166
+ end
167
+
168
+ def to_s
169
+ "BindingBuilder @tag=#{@tag}, @method=#{@method}"
170
+ end
171
+ end
172
+
173
+ class AtomBuilder
174
+ attr_accessor :tag, :name, :bindings, :clazz, :block
175
+
176
+ def initialize(name)
177
+ @name = name
178
+ @clazz = nil
179
+ @tag = nil
180
+ @bindings = []
181
+ @block = lambda {|x| true}
182
+ end
183
+
184
+ def method_missing(method_id, *args, &block)
185
+ if method_id == :not
186
+ return NotOperatorBuilder.new(@name)
187
+ end
188
+ end
189
+
190
+ def ==(value)
191
+ create_block value, lambda {|x,y| x == y}, lambda {|x| x == value}; self
192
+ end
193
+
194
+ def >(value)
195
+ create_block value, lambda {|x,y| x > y}, lambda {|x| x > value}; self
196
+ end
197
+
198
+ def <(value)
199
+ create_block value, lambda {|x,y| x < y}, lambda {|x| x < value}; self
200
+ end
201
+
202
+ def =~(value)
203
+ create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
204
+ end
205
+
206
+ def <=(value)
207
+ create_block value, lambda {|x,y| x <= y}, lambda {|x| x <= value}; self
208
+ end
209
+
210
+ def >=(value)
211
+ create_block value, lambda {|x,y| x >= y}, lambda {|x| x >= value}; self
212
+ end
213
+
214
+ def build_atom(tags=nil,methods=nil,when_id=nil)
215
+
216
+ raise 'Syntax error in Rules: class not set' unless @clazz
217
+
218
+ return Core::TypeAtom.new(@tag, @clazz) unless tags || when_id
219
+
220
+ return Core::PropertyAtom.new(@tag, @name, @clazz, &@block) if @bindings.empty?
221
+
222
+ if references_self?(tags,when_id)
223
+ bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
224
+ Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@clazz,&@block)
225
+ else
226
+ bind_tags = @bindings.collect{ |bb| bb.tag }
227
+ Core::ReferenceAtom.new(@tag,@name,bind_tags,@clazz,&@block)
228
+ end
229
+ end
230
+
231
+ private
232
+ def references_self?(tags,when_id)
233
+ ref_self = 0
234
+ @bindings.each do |bb|
235
+ if (tags[bb.tag] == when_id)
236
+ ref_self += 1
237
+ end
238
+ end
239
+
240
+ if ref_self > 0 and ref_self != @bindings.size
241
+ raise 'Binding to self and another pattern in the same condition is not yet supported.'
242
+ end
243
+
244
+ return ref_self > 0
245
+ end
246
+
247
+ def create_block(value, ref_block, basic_block)
248
+ if value && value.kind_of?(BindingBuilder)
249
+ @bindings = [value]
250
+ @block = ref_block
251
+ else
252
+ @block = basic_block
253
+ end
254
+ end
255
+ end
256
+
257
+ class NotOperatorBuilder < AtomBuilder
258
+ def ==(value)
259
+ create_block value, lambda {|x,y| x != y}, lambda {|x| x != value}; self
260
+ end
261
+ end
262
+ end
263
+ end