ruleby 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/benchmarks/basic_rules.rb +66 -0
- data/benchmarks/joined_rules.rb +73 -0
- data/benchmarks/miss_manners/data.rb +11 -0
- data/benchmarks/miss_manners/miss_manners.rb +11 -1
- data/benchmarks/miss_manners/model.rb +11 -0
- data/benchmarks/miss_manners/rules.rb +49 -110
- data/benchmarks/model.rb +36 -0
- data/examples/example_diagnosis.rb +35 -73
- data/examples/example_hello.rb +20 -22
- data/examples/example_politician.rb +22 -11
- data/examples/example_ticket.rb +40 -85
- data/examples/fibonacci_example1.rb +13 -2
- data/examples/fibonacci_example2.rb +11 -0
- data/examples/fibonacci_rulebook.rb +58 -111
- data/examples/test_self_reference.rb +51 -9
- data/lib/core/atoms.rb +53 -116
- data/lib/core/engine.rb +96 -96
- data/lib/core/nodes.rb +330 -298
- data/lib/core/patterns.rb +36 -39
- data/lib/core/utils.rb +141 -3
- data/lib/dsl/ferrari.rb +263 -0
- data/lib/dsl/letigre.rb +212 -0
- data/lib/dsl/steel.rb +313 -0
- data/lib/rulebook.rb +82 -265
- data/lib/ruleby.rb +13 -0
- metadata +21 -19
- data/benchmarks/50_joined_rules.rb +0 -78
- data/benchmarks/50_rules.rb +0 -57
- data/benchmarks/5_joined_rules.rb +0 -78
- data/benchmarks/5_rules.rb +0 -57
@@ -1,6 +1,18 @@
|
|
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
|
+
|
1
12
|
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
|
2
13
|
require 'ruleby'
|
3
|
-
|
14
|
+
|
15
|
+
include Ruleby
|
4
16
|
|
5
17
|
class Message
|
6
18
|
def initialize(status,message)
|
@@ -12,22 +24,52 @@ class Message
|
|
12
24
|
end
|
13
25
|
|
14
26
|
class SelfRefRulebook < Rulebook
|
15
|
-
def rules
|
16
|
-
rule
|
27
|
+
def rules
|
28
|
+
rule :LeTigreTest,
|
29
|
+
'For each Message as :m where #message as :x #&& #status == #:x' do |r,v|
|
30
|
+
puts 'Success'
|
31
|
+
end
|
32
|
+
|
33
|
+
rule :LeTigreTest,
|
34
|
+
[Message, :m, {m.message => :x}, m.status == b(:x)] do |r,v|
|
35
|
+
puts 'Success'
|
36
|
+
end
|
37
|
+
|
38
|
+
rule :BlueSteelTest do |r|
|
17
39
|
r.when do |has|
|
18
|
-
has.
|
19
|
-
has.
|
20
|
-
has.
|
40
|
+
has.m Message
|
41
|
+
has.m.message :x
|
42
|
+
has.m.status = :x, :%
|
21
43
|
end
|
22
|
-
r.then
|
44
|
+
r.then do |e,vars|
|
23
45
|
puts 'Success'
|
24
46
|
end
|
25
47
|
end
|
48
|
+
|
49
|
+
# NOTE references the self class binding is not allowed yet
|
50
|
+
|
51
|
+
# rule 'LeTigreTest',
|
52
|
+
# 'exists? Message as :m where #status == #:m.message' do |r,v|
|
53
|
+
# puts 'Success'
|
54
|
+
# end
|
55
|
+
|
56
|
+
# rule 'LeTigreTest',
|
57
|
+
# [Message, :m, m.status(:m, &c{|s,m| s == m.message})] do |r,v|
|
58
|
+
# puts 'Success'
|
59
|
+
# end
|
60
|
+
|
61
|
+
# rule :BlueSteelTest do |r|
|
62
|
+
# r.when do |has|
|
63
|
+
# has.m Message
|
64
|
+
# has.m.status :m do |s,m| s == m.message end
|
65
|
+
# end
|
66
|
+
# r.then do |e,vars|
|
67
|
+
# puts 'Success'
|
68
|
+
# end
|
69
|
+
# end
|
26
70
|
end
|
27
71
|
end
|
28
72
|
|
29
|
-
include Ruleby
|
30
|
-
|
31
73
|
engine :engine do |e|
|
32
74
|
SelfRefRulebook.new(e).rules
|
33
75
|
e.assert Message.new(:HELLO, :HELLO)
|
data/lib/core/atoms.rb
CHANGED
@@ -1,27 +1,46 @@
|
|
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
|
#tokens
|
2
13
|
module Ruleby
|
3
14
|
module Core
|
4
15
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
16
|
+
class Atom
|
17
|
+
attr_reader:name
|
18
|
+
attr_reader:tag
|
19
|
+
attr_reader:proc
|
20
|
+
attr_reader:clazz
|
21
|
+
|
22
|
+
def initialize(tag, name, clazz, &block)
|
11
23
|
@tag = tag
|
12
24
|
@name = name
|
25
|
+
@clazz = clazz
|
13
26
|
@proc = Proc.new(&block) if block_given?
|
14
|
-
|
27
|
+
|
28
|
+
# QUESTION we should probably change the '@clazz' variable to be called
|
29
|
+
# something else. Like '@deftemplate' maybe?
|
30
|
+
|
31
|
+
# QUESTION we should probably change the '@name' variable to be called
|
32
|
+
# something else. Like '@method' maybe?
|
33
|
+
end
|
15
34
|
|
16
|
-
|
17
|
-
|
18
|
-
|
35
|
+
def to_s
|
36
|
+
return "#{self.class}, tag=#{@tag}, name=#{@name}, class=#{@clazz}"
|
37
|
+
end
|
19
38
|
end
|
20
39
|
|
21
40
|
# This kind of atom is used to match just a single, hard coded value.
|
22
41
|
# For example:
|
23
42
|
#
|
24
|
-
# a.name
|
43
|
+
# a.name = 'John'
|
25
44
|
#
|
26
45
|
# So there are no references to other atoms.
|
27
46
|
class PropertyAtom < Atom
|
@@ -30,11 +49,7 @@ module Ruleby
|
|
30
49
|
end
|
31
50
|
|
32
51
|
def shareable?(atom)
|
33
|
-
return atom && @name == atom.name && @proc == atom.proc
|
34
|
-
end
|
35
|
-
|
36
|
-
def to_s
|
37
|
-
return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
|
52
|
+
return atom && @name == atom.name && @clazz = atom.clazz && @proc == atom.proc
|
38
53
|
end
|
39
54
|
end
|
40
55
|
|
@@ -43,126 +58,48 @@ module Ruleby
|
|
43
58
|
# p.has Person
|
44
59
|
#
|
45
60
|
# It is only used at the start of a pattern.
|
46
|
-
class TypeAtom <
|
47
|
-
|
61
|
+
class TypeAtom < Atom
|
62
|
+
def initialize(tag, clazz)
|
63
|
+
super tag, :class, clazz do |t| t == clazz end
|
64
|
+
end
|
65
|
+
|
66
|
+
def shareable?(atom)
|
67
|
+
return atom && @clazz = atom.clazz
|
68
|
+
end
|
48
69
|
end
|
49
70
|
|
50
71
|
# This kind of atom is used for matching a value that is a variable.
|
51
72
|
# For example:
|
52
73
|
#
|
53
|
-
# a.name
|
74
|
+
# a.name = :your_name, :%
|
54
75
|
#
|
55
76
|
# The expression for this atom depends on some other atom.
|
56
|
-
class ReferenceAtom < Atom
|
57
|
-
|
58
|
-
|
59
|
-
|
77
|
+
class ReferenceAtom < Atom
|
78
|
+
attr_reader :vars
|
79
|
+
|
80
|
+
def initialize(tag, name, vars, clazz, &block)
|
81
|
+
super(tag, name, clazz, &block)
|
60
82
|
@vars = vars # list of referenced variable names
|
61
|
-
end
|
83
|
+
end
|
62
84
|
|
63
|
-
|
85
|
+
def shareable?(atom)
|
86
|
+
false
|
87
|
+
end
|
64
88
|
|
65
89
|
def ==(atom)
|
66
90
|
return atom.kind_of?(ReferenceAtom) && @proc == atom.proc && @tag == atom.tag && @vars == atom.vars
|
67
91
|
end
|
68
92
|
|
69
93
|
def to_s
|
70
|
-
return
|
94
|
+
return super + ", vars=#{vars.join(',')}"
|
71
95
|
end
|
72
96
|
end
|
73
97
|
|
74
98
|
# This is an atom that references another atom that is in the same pattern.
|
99
|
+
# Note that in a SelfReferenceAtom, the 'vars' argument must be a list of the
|
100
|
+
# *methods* that this atom references (not the variable names)!
|
75
101
|
class SelfReferenceAtom < ReferenceAtom
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
class MatchResult
|
80
|
-
|
81
|
-
# TODO this class needs to be cleaned up for that we don't have a bunch of
|
82
|
-
# properties. Instead, maybe it sould have a list of facts.
|
83
|
-
def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
|
84
|
-
@variables = variables
|
85
|
-
|
86
|
-
# a list of recencies of the facts that this matchresult depends on.
|
87
|
-
@recency = recency
|
88
|
-
|
89
|
-
# notes where this match result is from a NotPattern or ObjectPattern
|
90
|
-
# TODO this isn't really needed anymore. how can we get rid of it?
|
91
|
-
@is_match = is_match
|
92
|
-
|
93
|
-
# a hash of fact.ids that each tag corresponds to
|
94
|
-
# QUESTION what is better: this or the @backward_hash in MatchContext?
|
95
|
-
# Right now these two are somewhat redundate.
|
96
|
-
@fact_hash = fact_hash
|
97
|
-
end
|
98
|
-
def []=(sym, object)
|
99
|
-
@variables[sym] = object
|
100
|
-
end
|
101
|
-
def [](sym)
|
102
|
-
return @variables[sym]
|
103
|
-
end
|
104
|
-
def fact_ids
|
105
|
-
return fact_hash.values.uniq
|
106
|
-
end
|
107
|
-
attr :variables, true
|
108
|
-
attr :is_match, true
|
109
|
-
attr :fact_hash, true
|
110
|
-
attr :resolved, true
|
111
|
-
attr :recency, true
|
112
|
-
def ==(match)
|
113
|
-
return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
|
114
|
-
end
|
115
|
-
|
116
|
-
def key?(m)
|
117
|
-
return @variables.key?(m)
|
118
|
-
end
|
119
|
-
|
120
|
-
def keys
|
121
|
-
return @variables.keys
|
122
|
-
end
|
123
|
-
|
124
|
-
def update(mr)
|
125
|
-
@recency = @recency | mr.recency
|
126
|
-
@is_match = mr.is_match
|
127
|
-
@variables = @variables.update mr.variables
|
128
|
-
|
129
|
-
# QUESTION why the heck does this statement work instead of the
|
130
|
-
# commented one below it??
|
131
|
-
@fact_hash = mr.fact_hash.update @fact_hash
|
132
|
-
#@fact_hash = @fact_hash.update mr.fact_hash
|
133
|
-
|
134
|
-
return self
|
135
|
-
end
|
136
|
-
|
137
|
-
def dup
|
138
|
-
dup_mr = MatchResult.new
|
139
|
-
dup_mr.recency = @recency.clone
|
140
|
-
dup_mr.is_match = @is_match
|
141
|
-
dup_mr.variables = @variables.clone
|
142
|
-
dup_mr.fact_hash = @fact_hash.clone
|
143
|
-
return dup_mr
|
144
|
-
end
|
145
|
-
|
146
|
-
def clear
|
147
|
-
@variables = {}
|
148
|
-
@fact_hash = {}
|
149
|
-
@recency = []
|
150
|
-
end
|
151
|
-
|
152
|
-
def delete(tag)
|
153
|
-
@variables.delete(tag)
|
154
|
-
@fact_hash.delete(tag)
|
155
|
-
end
|
156
|
-
|
157
|
-
def to_s
|
158
|
-
s = '#MatchResult('
|
159
|
-
s = s + 'f)(' unless @is_match
|
160
|
-
s = s + object_id.to_s+')('
|
161
|
-
@variables.each do |key,value|
|
162
|
-
s += "#{key}=#{value}/#{@fact_hash[key]}, "
|
163
|
-
end
|
164
|
-
return s + ")"
|
165
|
-
end
|
166
102
|
end
|
103
|
+
|
167
104
|
end
|
168
105
|
end
|
data/lib/core/engine.rb
CHANGED
@@ -1,4 +1,14 @@
|
|
1
|
-
#
|
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
|
+
|
2
12
|
require 'core/atoms'
|
3
13
|
require 'core/patterns'
|
4
14
|
require 'core/utils'
|
@@ -6,17 +16,20 @@ require 'core/nodes'
|
|
6
16
|
|
7
17
|
module Ruleby
|
8
18
|
module Core
|
9
|
-
|
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
|
+
|
10
27
|
def initialize(&block)
|
11
28
|
@name = nil
|
12
29
|
@proc = Proc.new(&block) if block_given?
|
13
30
|
@priority = 0
|
14
31
|
end
|
15
|
-
|
16
|
-
attr_accessor :priority
|
17
|
-
attr_accessor :name
|
18
|
-
attr_reader :matches
|
19
|
-
|
32
|
+
|
20
33
|
def fire(r, match)
|
21
34
|
@proc.call(r, match)
|
22
35
|
end
|
@@ -26,19 +39,24 @@ module Ruleby
|
|
26
39
|
end
|
27
40
|
end
|
28
41
|
|
29
|
-
|
30
|
-
|
42
|
+
# An activation is an action/match pair that is executed if a rule is matched.
|
43
|
+
# It also contains metadata that can be used for conflict resolution if two
|
44
|
+
# rules are satisfied by the same fact.
|
45
|
+
class Activation
|
46
|
+
attr_reader :action, :match
|
47
|
+
attr_accessor :counter, :used
|
48
|
+
|
49
|
+
def initialize(action, match, counter=0)
|
31
50
|
@action = action
|
32
51
|
@match = match
|
33
52
|
@match.recency.sort!
|
34
53
|
@match.recency.reverse!
|
35
|
-
@counter =
|
36
|
-
|
37
|
-
|
38
|
-
attr_reader :action, :match
|
39
|
-
attr_accessor :counter
|
54
|
+
@counter = counter
|
55
|
+
@used = false
|
56
|
+
end
|
40
57
|
|
41
58
|
def fire(r)
|
59
|
+
@used = true
|
42
60
|
@action.fire r, @match
|
43
61
|
end
|
44
62
|
|
@@ -62,41 +80,16 @@ module Ruleby
|
|
62
80
|
end
|
63
81
|
end
|
64
82
|
|
65
|
-
class Rule
|
83
|
+
class Rule
|
84
|
+
attr_accessor :pattern
|
85
|
+
attr_reader :action, :name, :priority
|
86
|
+
|
66
87
|
def initialize(name, pattern=nil, action=nil, priority=0)
|
67
88
|
@name = name
|
68
89
|
@pattern = pattern
|
69
90
|
@action = action
|
70
91
|
@priority = priority
|
71
92
|
end
|
72
|
-
|
73
|
-
attr_accessor:pattern
|
74
|
-
attr_reader:action, :name
|
75
|
-
|
76
|
-
|
77
|
-
def when(&block)
|
78
|
-
wb = WhenBuilder.new
|
79
|
-
yield wb
|
80
|
-
@pattern = wb.pattern
|
81
|
-
end
|
82
|
-
|
83
|
-
def then(&block)
|
84
|
-
@action = Core::Action.new(&block)
|
85
|
-
end
|
86
|
-
|
87
|
-
def when=(pattern)
|
88
|
-
@pattern = pattern
|
89
|
-
end
|
90
|
-
|
91
|
-
def then=(action)
|
92
|
-
@action = action
|
93
|
-
@action.name = @name
|
94
|
-
@action.priority = @priority
|
95
|
-
end
|
96
|
-
|
97
|
-
def priority
|
98
|
-
return @priority
|
99
|
-
end
|
100
93
|
|
101
94
|
def priority=(p)
|
102
95
|
@priority = p
|
@@ -104,16 +97,18 @@ module Ruleby
|
|
104
97
|
end
|
105
98
|
end
|
106
99
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
@object = object
|
111
|
-
end
|
112
|
-
|
100
|
+
# A fact is an object that is stored in working memory. The rules in the
|
101
|
+
# system will either look for the existence or absence of particular facts.
|
102
|
+
class Fact
|
113
103
|
attr :token, true
|
114
104
|
attr :recency, true
|
115
105
|
attr_reader :object
|
116
106
|
|
107
|
+
def initialize(object, token)
|
108
|
+
@token = token
|
109
|
+
@object = object
|
110
|
+
end
|
111
|
+
|
117
112
|
def id
|
118
113
|
return object.object_id
|
119
114
|
end
|
@@ -127,12 +122,18 @@ module Ruleby
|
|
127
122
|
end
|
128
123
|
end
|
129
124
|
|
130
|
-
|
125
|
+
# A conflict resolver is used to order activations that become active at the
|
126
|
+
# same time. The default implementation sorts the agenda based on the
|
127
|
+
# properties of the activation.
|
128
|
+
class RulebyConflictResolver
|
131
129
|
def resolve(agenda)
|
132
130
|
return agenda.sort
|
133
|
-
end
|
131
|
+
end
|
134
132
|
end
|
135
133
|
|
134
|
+
# The working memory is a container for all the facts in the system. The
|
135
|
+
# inference engine will compare these facts with the rules to produce some
|
136
|
+
# outcomes.
|
136
137
|
class WorkingMemory
|
137
138
|
def initialize
|
138
139
|
@recency = 0
|
@@ -170,65 +171,59 @@ module Ruleby
|
|
170
171
|
end
|
171
172
|
end
|
172
173
|
|
174
|
+
# This is the core class of the library. A new rule engine is create by
|
175
|
+
# instantiating it. Each rule engine has one inference engine, one rule set
|
176
|
+
# and one working memory.
|
173
177
|
class Engine
|
174
|
-
def initialize(wm=WorkingMemory.new)
|
178
|
+
def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new)
|
175
179
|
@root = nil
|
176
180
|
@working_memory = wm
|
177
|
-
@conflict_resolver =
|
181
|
+
@conflict_resolver = cr
|
182
|
+
@wm_altered = false
|
178
183
|
end
|
179
|
-
|
184
|
+
|
185
|
+
# This method id called to add a new fact to working memory
|
180
186
|
def assert(object,&block)
|
187
|
+
@wm_altered = true
|
181
188
|
fact_helper(object,:plus,&block)
|
182
189
|
end
|
183
|
-
|
190
|
+
|
191
|
+
# This method is called to remove an existing fact from working memory
|
184
192
|
def retract(object,&block)
|
193
|
+
@wm_altered = true
|
185
194
|
fact_helper(object,:minus,&block)
|
186
195
|
end
|
187
|
-
|
188
|
-
#
|
189
|
-
#
|
196
|
+
|
197
|
+
# This method is called to alter an existing fact. It is essentially a
|
198
|
+
# retract followed by an assert.
|
190
199
|
def modify(object,&block)
|
191
200
|
retract(object,&block)
|
192
201
|
assert(object,&block)
|
193
202
|
end
|
194
203
|
|
195
|
-
|
196
|
-
|
197
|
-
@root.assert_fact wm_fact if @root != nil
|
198
|
-
end
|
199
|
-
|
200
|
-
def assert_rule(rule)
|
204
|
+
# This method adds a new rule to the system.
|
205
|
+
def assert_rule(rule)
|
201
206
|
@root = RootNode.new(@working_memory) if @root == nil
|
202
207
|
@root.assert_rule rule
|
203
208
|
end
|
204
209
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
210
|
+
# This method executes the activations that were generated by the rules
|
211
|
+
# that match facts in working memory.
|
212
|
+
def match(agenda=nil, used_agenda=[])
|
213
|
+
if @root
|
214
|
+
@root.reset_counter
|
215
|
+
agenda = @root.matches unless agenda
|
216
|
+
while (agenda.length > 0)
|
217
|
+
agenda = @conflict_resolver.resolve agenda
|
218
|
+
activation = agenda.pop
|
219
|
+
used_agenda.push activation
|
220
|
+
activation.fire self
|
221
|
+
if @wm_altered
|
222
|
+
agenda = @root.matches(false)
|
223
|
+
@root.increment_counter
|
224
|
+
@wm_altered = false
|
220
225
|
end
|
221
|
-
|
222
|
-
# BUG we are comparing against the current agenda, but we may need to
|
223
|
-
# compare against all activations that have existed...
|
224
|
-
if (agenda.index(a) == nil) && (!used)
|
225
|
-
a.counter = activation_counter+1
|
226
|
-
agenda.push a
|
227
|
-
end
|
228
|
-
end
|
229
|
-
agenda.delete_if {|a| new_agenda.index(a) == nil}
|
230
|
-
|
231
|
-
activation_counter = activation_counter+1
|
226
|
+
end
|
232
227
|
end
|
233
228
|
end
|
234
229
|
|
@@ -238,12 +233,17 @@ module Ruleby
|
|
238
233
|
end
|
239
234
|
|
240
235
|
private
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
236
|
+
def fact_helper(object, sign=:plus, &block)
|
237
|
+
f = Core::Fact.new object, sign
|
238
|
+
yield f if block_given?
|
239
|
+
assert_fact f
|
240
|
+
f
|
241
|
+
end
|
242
|
+
|
243
|
+
def assert_fact(fact)
|
244
|
+
wm_fact = @working_memory.assert_fact fact
|
245
|
+
@root.assert_fact wm_fact if @root != nil
|
246
|
+
end
|
247
247
|
end
|
248
248
|
end
|
249
249
|
end
|