wongi-engine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +349 -0
  5. data/Rakefile +2 -0
  6. data/examples/ex01.rb +23 -0
  7. data/examples/ex02.rb +36 -0
  8. data/examples/graphviz.rb +15 -0
  9. data/examples/timeline.rb +48 -0
  10. data/lib/wongi-engine.rb +22 -0
  11. data/lib/wongi-engine/alpha_memory.rb +46 -0
  12. data/lib/wongi-engine/beta.rb +10 -0
  13. data/lib/wongi-engine/beta/beta_memory.rb +48 -0
  14. data/lib/wongi-engine/beta/beta_node.rb +164 -0
  15. data/lib/wongi-engine/beta/filter_node.rb +109 -0
  16. data/lib/wongi-engine/beta/join_node.rb +127 -0
  17. data/lib/wongi-engine/beta/ncc_node.rb +46 -0
  18. data/lib/wongi-engine/beta/ncc_partner.rb +43 -0
  19. data/lib/wongi-engine/beta/neg_node.rb +58 -0
  20. data/lib/wongi-engine/beta/optional_node.rb +43 -0
  21. data/lib/wongi-engine/beta/or_node.rb +76 -0
  22. data/lib/wongi-engine/beta/production_node.rb +31 -0
  23. data/lib/wongi-engine/core_ext.rb +57 -0
  24. data/lib/wongi-engine/dsl.rb +112 -0
  25. data/lib/wongi-engine/dsl/action.rb +12 -0
  26. data/lib/wongi-engine/dsl/actions/error_generator.rb +42 -0
  27. data/lib/wongi-engine/dsl/actions/simple_action.rb +23 -0
  28. data/lib/wongi-engine/dsl/actions/simple_collector.rb +51 -0
  29. data/lib/wongi-engine/dsl/actions/statement_generator.rb +52 -0
  30. data/lib/wongi-engine/dsl/actions/trace_action.rb +52 -0
  31. data/lib/wongi-engine/dsl/any_rule.rb +48 -0
  32. data/lib/wongi-engine/dsl/dsl_builder.rb +44 -0
  33. data/lib/wongi-engine/dsl/dsl_extensions.rb +43 -0
  34. data/lib/wongi-engine/dsl/extension_clause.rb +36 -0
  35. data/lib/wongi-engine/dsl/generation_clause.rb +15 -0
  36. data/lib/wongi-engine/dsl/generic_production_rule.rb +78 -0
  37. data/lib/wongi-engine/dsl/ncc_production_rule.rb +21 -0
  38. data/lib/wongi-engine/dsl/production_rule.rb +4 -0
  39. data/lib/wongi-engine/dsl/query.rb +24 -0
  40. data/lib/wongi-engine/graph.rb +71 -0
  41. data/lib/wongi-engine/model_context.rb +13 -0
  42. data/lib/wongi-engine/network.rb +416 -0
  43. data/lib/wongi-engine/network/collectable.rb +42 -0
  44. data/lib/wongi-engine/network/debug.rb +25 -0
  45. data/lib/wongi-engine/ruleset.rb +74 -0
  46. data/lib/wongi-engine/template.rb +111 -0
  47. data/lib/wongi-engine/token.rb +137 -0
  48. data/lib/wongi-engine/version.rb +5 -0
  49. data/lib/wongi-engine/wme.rb +134 -0
  50. data/lib/wongi-engine/wme_match_data.rb +34 -0
  51. data/spec/dataset_spec.rb +26 -0
  52. data/spec/dsl_spec.rb +9 -0
  53. data/spec/high_level_spec.rb +341 -0
  54. data/spec/ruleset_spec.rb +54 -0
  55. data/spec/simple_action_spec.rb +40 -0
  56. data/spec/spec_helper.rb +1 -0
  57. data/spec/wme_spec.rb +83 -0
  58. data/wongi-engine.gemspec +19 -0
  59. metadata +110 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wongi-engine.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Valeri Sokolov <ulfurinn@ulfurinn.net>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,349 @@
1
+ # Wongi::Engine
2
+
3
+ This library contains a rule engine written in Ruby. It's based on the [Rete algorithm](http://en.wikipedia.org/wiki/Rete_algorithm) and uses a DSL to express rules in a readable way.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'wongi-engine'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install wongi-engine
18
+
19
+ ## Tutorial
20
+
21
+ To begin, say
22
+
23
+ engine = Wongi::Engine.create
24
+
25
+ Now let's add some facts to the system.
26
+
27
+ ### Facts
28
+
29
+ All knowledge in Wongi::Engine is represented as triples of { subject, predicate, object }. Predicates usually stand for subjects' properties, and objects for values of those properties. More complex types can always be decomposed into such triples.
30
+
31
+ Triples can contain any Ruby object that defines the `==` comparison in a meaningful way, but some symbols have special meaning, as we will see.
32
+
33
+ Try this:
34
+
35
+ engine << [ "Alice", "friend", "Bob" ]
36
+ engine << [ "Alice", "age", 35 ]
37
+
38
+ What can we do with this information?
39
+
40
+ ### Simple iteration
41
+
42
+ Suppose we want to list all we know about Alice. You could, for instance, do:
43
+
44
+ engine.each "Alice", :_, :_ do |item|
45
+ puts "Alice's #{item.predicate} is #{item.object}"
46
+ end
47
+
48
+ `each` takes three arguments for every field of a triple and tries to match the resulting template against the known facts. `:_` is the special value that matches anything. This kind of pattern matching plays a large role in Wongi::Engine; more on that later.
49
+
50
+ In a similar way, you can use `select` to get an array of matching facts and `find` to get the first matching one. Both methods take three arguments.
51
+
52
+ ### Simple rules
53
+
54
+ It's not very interesting to use the engine like that, though. Rule engines are supposed to be declarative. Let's try this:
55
+
56
+ friends = engine.rule "friends" do
57
+ forall {
58
+ has :PersonA, "friend", :PersonB
59
+ }
60
+ end
61
+
62
+ Here's your first taste of the engine's DSL. A rule, generally speaking, consists of a number of conditions the dataset needs to meet; those are defined in the `forall` section (also spelled `for_all`, if you prefer that). `has` (or `fact`) specifies that there needs to be a fact that matches the given pattern; in this case, one with the predicate `"friends"`.
63
+
64
+ When a pattern contains a symbol that starts with an uppercase letter, it introduces a variable which will be bound to an actual triple field. Their values can be retrieved from the result set:
65
+
66
+ friends.tokens.each do |token|
67
+ puts "%s and %s are friends" % [ token[ :PersonA ], token[ :PersonB ] ]
68
+ end
69
+
70
+ A **token** represents all facts that passed the rule's conditions. If you think of the dataset as of a long SQL table being joined with itself, then a token is like a row in the resulting table.
71
+
72
+ If you don't care about a specific field's value, you can use the all-matcher `:_` in its place so as not to introduce unnecessary variables.
73
+
74
+ Once a variable is bound, it can be used to match further facts within a rule. Let's add another friendship:
75
+
76
+ engine << [ "Bob", "friend", "Claire" ]
77
+
78
+ and another rule:
79
+
80
+ remote = engine.rule "remote friends" do
81
+ forall {
82
+ has :PersonA, "friend", :PersonB
83
+ has :PersonB, "friend", :PersonC
84
+ }
85
+ end
86
+
87
+ remote.tokens.each do |token|
88
+ puts "%s and %s are friends through %s" % [ token[ :PersonA ], token[ :PersonC ], token[ :PersonB ] ]
89
+ end
90
+
91
+ (`engine.rule` returns the created **production node** - an object that accumulates the rule's result set. You don't have to carry it around if you don't want to - it is always possible to retrieve it later as `engine.productions["remote friends"]`.)
92
+
93
+ ### Stored queries
94
+
95
+ Taking the SQL metaphor further, you can use the engine to do fancy searches:
96
+
97
+ q = engine.query "friends" do
98
+ search_on :Name
99
+ forall {
100
+ has :Name, "friend", :Friend
101
+ }
102
+ end
103
+
104
+ engine.execute "friends", { Name: "Alice" }
105
+ q.tokens.each do |token|
106
+ ... # you know the drill
107
+ end
108
+
109
+ Not that this is a particularly fancy search, but you get the idea.
110
+
111
+ Queries work the same way as normal rules, but they come with some variables already bound by the time matching starts.
112
+
113
+ You can also retrieve the query's production node from `engine.results["friends"]` (they are intentionally kept separate from regular productions).
114
+
115
+ ### Taking an action
116
+
117
+ There's more to rules than passive accumulation:
118
+
119
+ engine.rule "self-printer" do
120
+ forall {
121
+ has :PersonA, "friend", :PersonB
122
+ }
123
+ make {
124
+ action { |token|
125
+ puts "%s and %s are friends" % [ token[ :PersonA ], token[ :PersonB ] ]
126
+ }
127
+ }
128
+ end
129
+
130
+ The `make` section (also spelled `do!`, if you find it more agreeable English, because `do` is a keyword in Ruby) lists everything that happens when a rule's conditions are fully matched (we say the production node is activated). Wongi::Engine provides only a small amount of build-in actions, but you can define define your own ones, and the simplest one is just `action` with a block.
131
+
132
+ ### More facts!
133
+
134
+ Note how our facts define relations that always go from subject to object - they form a directed graph. In a perfect world, friendships go both ways, but to specify this in out model, we need to have two facts for each couple. Instead of duplicating everything by hand, let's automate that:
135
+
136
+ engine.rule "symmetric predicate" do
137
+ forall {
138
+ has :Predicate, "symmetric", true
139
+ has :X, :Predicate, :Y
140
+ }
141
+ make {
142
+ gen :Y, :Predicate, :X
143
+ }
144
+ end
145
+
146
+ engine << ["friend", "symmetric", true]
147
+
148
+ If you still have the "self-printer" rule installed, you will see some new friendships pop up immediately!
149
+
150
+ The built-in `gen` action creates new facts, taking either fixed values or variables as arguments. (It will complain if use provide a variable that isn't bound by the time it's activated.) Here, it takes all relations we've defined to be [symmetric](http://en.wikipedia.org/wiki/Symmetric_relation), finds all couples in those sorts of relations and turns them around.
151
+
152
+ ### Matchers
153
+
154
+ It wouldn't be very useful if `has` were the only sort of condition that could be used. Here are some more:
155
+
156
+ #### `neg subject, predicate, object`
157
+
158
+ Passes if the specified template does *not* match anything in the dataset. Alias: `missing`.
159
+
160
+ #### `maybe subject, predicate, object`
161
+
162
+ Passes whether or not the template matches anything. It's only useful if it introduces a new variable. Alias: `optional`.
163
+
164
+ #### `none { ... }`
165
+
166
+ The `none` block contains other matchers and passes if that *entire subchain* returns an empty set. In other words, it corresponds to an expression `not ( a and b and ... )`.
167
+
168
+ #### `any { variant { ... } ... }`
169
+
170
+ The `any` block contains several `variant` blocks, each of them containing other matchers. It passes if any of the `variant` subchains matches. It's a shame that disjunction has to be so much more verbose than conjunction, but life is cruel.
171
+
172
+ #### `same x, y`
173
+
174
+ Passes if the arguments are equal. Alias: `eq`, `equal`.
175
+
176
+ #### `diff x, y`
177
+
178
+ Passes if the arguments are not equal. Alias: `ne`.
179
+
180
+ ### Timeline
181
+
182
+ Wongi::Engine has a limited concept of timed facts: time is discrete and only extends into the past. Matchers that accept a triple specification (`has`, `neg` and `maybe`) can also accept a fourth parameter, an integer <= 0, which will make them look at a past state of the system. "0" means the current state and is the default value, "-1" means the one just before the current, and so on.
183
+
184
+ To create past states, say:
185
+
186
+ engine.snapshot!
187
+
188
+ This will shift all facts one step into the past. The new current state will be a copy of the last one. You can only insert new facts into the current state, "retroactive" facts are not allowed.
189
+
190
+ ### Time-aware matchers
191
+
192
+ The following matchers are nothing but syntactic sugar for a combination of primitives.
193
+
194
+ #### `asserted subject, predicate, object`
195
+
196
+ Short for:
197
+
198
+ neg subject, predicate, object, -1
199
+ has subject, predicate, object, 0
200
+
201
+ That is, it passes if the fact was missing in the previous state but exists in the current one. Alias: `added`.
202
+
203
+ #### `retracted subject, predicate, object`
204
+
205
+ Short for:
206
+
207
+ has subject, predicate, object, -1
208
+ neg subject, predicate, object, 0
209
+
210
+ The reverse of `asserted`. Alias: `removed`.
211
+
212
+ #### `kept subject, predicate, object`
213
+
214
+ Short for:
215
+
216
+ has subject, predicate, object, -1
217
+ has subject, predicate, object, 0
218
+
219
+ Alias: `still_has`.
220
+
221
+ #### `kept_missing subject, predicate, object`
222
+
223
+ Short for:
224
+
225
+ neg subject, predicate, object, -1
226
+ neg subject, predicate, object, 0
227
+
228
+ Alias: `still_missing`.
229
+
230
+ ### Other built-in actions
231
+
232
+ #### `collect variable, collector_name`
233
+
234
+ If you use this action, `engine.collection( collector_name )` will provide a `uniq`'ed array of all values `variable` has been bound to. It's a bit shorter than iterating over the tokens by hand.
235
+
236
+ #### `error message`, `error { |hash_of_variable_assignments| ... }`
237
+
238
+ Useful when you want to detect contradictory facts. `engine.errors` will give an array of all error messages produced when this action is activated. If you use the block form, the block needs to return a message.
239
+
240
+ #### `trace options`
241
+
242
+ The debugging action that will print a message every time it's activated. Possible options are:
243
+
244
+ * `values` (boolean = false): whether to print variable assignments as well
245
+ * `io` (IO = $stdout): which IO object to use
246
+ * `generation` (boolean = false): whether this rule's `gen` action should print messages too. `trace` must come before any `gen` actions in this case.
247
+ * `tracer`, `tracer_class`: a custom tracer that must respond to `trace` and accept a hash argument. Hash contents will vary depending on the action being traced.
248
+
249
+ ### Custom actions
250
+
251
+ We've seen one way to specify custom actions: using `action` with a block. Another way to use it is to say:
252
+
253
+ action class, ... do
254
+ ...
255
+ end
256
+
257
+ Any additional arguments or blocks will be given to `initialize`, and the class must define an `execute` method taking a token. Passing any object with an `execute` method also works.
258
+
259
+ If your action class inherits from `Wongi::Engine::Action`, you'll have the following (more or less useful) attributes:
260
+
261
+ * `rete`: the engine instance
262
+ * `rule`: the rule object that is using this action
263
+ * `name`: the extension clause used to define this action (read more under [DSL extensions])
264
+ * `production`: the production node
265
+
266
+ If you can't or don't want to inherit, you can define the accessors yourself. Having just the ones you need is fine.
267
+
268
+ ### Organising rules
269
+
270
+ Using `engine.rule` and `engine.query` is fine if you want to experiment, but to make rules and queries more manageable, you will probably want to keep them separate from the engine instance. One way to do that is to just say:
271
+
272
+ my_rule = rule "name" do
273
+ ...
274
+ end
275
+
276
+ engine << my_rule
277
+
278
+ For even more convenience, why not group rules together:
279
+
280
+ my_ruleset = ruleset {
281
+ rule "rule 1" do
282
+ ...
283
+ end
284
+ rule "rule 2" do
285
+ ...
286
+ end
287
+ }
288
+
289
+ engine << my_ruleset
290
+
291
+ Again, you don't need to hold on to object references if you don't want to:
292
+
293
+ ruleset "my set" do
294
+ ...
295
+ end
296
+
297
+ engine << Wongi::Engine::Ruleset[ "my set" ]
298
+
299
+ ### DSL extensions
300
+
301
+ This is a more advanced method of customising. In general, DSL extensions have the form:
302
+
303
+ dsl {
304
+ section [ :forall | :make ]
305
+ clause :my_action
306
+ [ action | accept | body ] ...
307
+ }
308
+
309
+ which is then used in a rule like this:
310
+
311
+ make {
312
+ my_action ...
313
+ }
314
+
315
+ DSL extensions are globally visible to all engine instances.
316
+
317
+ Let's have a look at the three ways to define a clause's implementation.
318
+
319
+ #### `body { |...| ... }`
320
+
321
+ This simply allows you to group several other actions or matchers. It is perhaps the only way you have to extend the `forall` section, as any non-trivial matchers will require special support from the engine itself.
322
+
323
+ #### `action class`, `action do |token| ... end`
324
+
325
+ This works almost exactly like using the `action` action directly in a rule, but gives it a more meaningful alias. Arguments to `initialize`, however, are taken from the action's invocation in `make`, not definition.
326
+
327
+ #### `accept class`
328
+
329
+ Most library users probably won't need this, but it's here for completion. Acceptors represent an intermediate state. They allow you to have some shared data that you customize for a given engine instance. The class needs to respond to `import_into( engine_instance )` and return something usable as an action, or to be usable as an action itself.
330
+
331
+ The class also gets arguments to `initialize` from the action's invocation.
332
+
333
+ ## Acknowledgements
334
+
335
+ The Rete implementation in this library largely follows the outline presented in [\[Doorenbos, 1995\]](http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf).
336
+
337
+ ## Changelog
338
+
339
+ ### 0.0.1
340
+
341
+ * initial repackaged release
342
+
343
+ ## Contributing
344
+
345
+ 1. Fork it
346
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
347
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
348
+ 4. Push to the branch (`git push origin my-new-feature`)
349
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/examples/ex01.rb ADDED
@@ -0,0 +1,23 @@
1
+ include Wongi::Engine
2
+
3
+ ds = Network.new
4
+
5
+ ds << WME.new( "Alice", "friend", "Bob" )
6
+
7
+ puts "Enumerate all:"
8
+
9
+ ds.each do |wme|
10
+ puts wme
11
+ end
12
+
13
+ puts "Enumerate by pattern:"
14
+
15
+ ds.each :_, "friend", :_ do |wme|
16
+ puts wme
17
+ end
18
+
19
+ puts "Mismatching pattern:"
20
+
21
+ ds.each :_, "foe", :_ do |wme|
22
+ puts wme
23
+ end
data/examples/ex02.rb ADDED
@@ -0,0 +1,36 @@
1
+ include Wongi::Engine
2
+
3
+ ds = Network.new
4
+
5
+ ds << ruleset {
6
+
7
+ name "Example"
8
+
9
+ rule "symmetric" do
10
+ forall {
11
+ has :P, "symmetric", true
12
+ has :A, :P, :B
13
+ }
14
+ make {
15
+ gen :B, :P, :A
16
+ }
17
+ end
18
+
19
+ }
20
+
21
+ puts "Installed ruleset"
22
+
23
+ ds << WME.new( "friend", "symmetric", true )
24
+ ds << WME.new( "Alice", "friend", "Bob" )
25
+
26
+ puts "Asserted facts:"
27
+
28
+ puts "Should print 3 facts:"
29
+ puts ds.wmes
30
+
31
+
32
+ ds.retract WME.new( "Alice", "friend", "Bob" )
33
+
34
+ puts "Should print 1 fact:"
35
+ puts ds.wmes
36
+