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