ruleby 0.2 → 0.3

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