clips 0 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rspec +2 -1
- data/README.md +42 -12
- data/Rakefile +19 -2
- data/clips.gemspec +17 -16
- data/lib/clips.rb +119 -3
- data/lib/clips/rule.rb +30 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3d39db49bfb6f9123fa8c21e90eb2fb6b898e24cffb22e95bbdcb3245314e9f6
|
4
|
+
data.tar.gz: e8ecc9cfdc1d829d4c30491c0fcadf79e924d8e03ac07bab9c56bc7fcd72b3a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a79b726cf9b0ef5be8dbefa531b61ebebb85427827a6ffa345ff56fcce9fb67197d631d785ae0ad31b616d2650d792a6ce96556e6f57da9df1c6b33bd393bbb
|
7
|
+
data.tar.gz: 671637f65a5c8a854f70be32769e8aa5985b60dc6845646a9230d5d47d54d9e99c67a26a691d0fdfbfb9cd8902d315118be682f9a6d88ffda8984c1a29f1a9ea
|
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,46 @@
|
|
1
|
-
#
|
1
|
+
# CLIPS for Ruby
|
2
2
|
|
3
|
-
|
3
|
+
This is a reimplemtation of the [CLIPS](http://www.clipsrules.net/) engine in Ruby, as well as a DSL for working with CLIPS rules.
|
4
4
|
|
5
|
-
|
5
|
+
The inference engine portion of this implementation has been structured to resemble Ruby's standard Set class. While this doesn't mirror the syntax used by the CLIPS language, it does stay true to the underlying functionality of the engine (ie. no duplicate facts).
|
6
|
+
|
7
|
+
The C implementation of CLIPS includes a command line shell, however this implementation does not. There are a million ways to make a shell-like interface in Ruby, none of which are appropriate to be included in this gem.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'clips'
|
13
|
+
|
14
|
+
clips = CLIPS.new
|
15
|
+
clips.add 'rule1', CLIPS::Rule.new(:foo, :actions => :bar)
|
16
|
+
clips.add :rule2, CLIPS::Rule.new(:bar) { puts "Rule 2!" }
|
17
|
+
clips.add :foo
|
18
|
+
|
19
|
+
clips.run # => "Rule 2!"
|
20
|
+
clips.facts # => #<Set: {[:foo], [:bar]}>
|
21
|
+
```
|
22
|
+
|
23
|
+
## Facts and Rules
|
24
|
+
|
25
|
+
Facts are represented by Arrays, and Rules are instances of the Rule class. The first element of a fact must always be a Symbol. Rule names can be given as Symbols or Strings, and are converted to Symbols internally.
|
26
|
+
|
27
|
+
## Default Facts
|
28
|
+
|
29
|
+
Named sets of default facts can be added to the engine. When `reset` is called, the default facts are asserted as facts.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
clips.default_facts['my_defaults'] = Set[[:foo], [:bar]]
|
33
|
+
clips.facts # => #<Set: {}>
|
34
|
+
...
|
35
|
+
clips.reset
|
36
|
+
clips.facts # => #<Set: {[:foo], [:bar]}>
|
37
|
+
```
|
38
|
+
|
39
|
+
To remove a set of default facts, use the normal Hash `delete` method.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
clips.default_facts.delete('my_defaults')
|
43
|
+
```
|
6
44
|
|
7
45
|
## Installation
|
8
46
|
|
@@ -20,16 +58,8 @@ Or install it yourself as:
|
|
20
58
|
|
21
59
|
$ gem install clips
|
22
60
|
|
23
|
-
## Usage
|
24
|
-
|
25
|
-
TODO: Write usage instructions here
|
26
|
-
|
27
61
|
## Development
|
28
62
|
|
29
63
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
64
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/bfoz/clips.
|
65
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `clips.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
data/Rakefile
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
|
-
RSpec::Core::RakeTask.new(:
|
4
|
+
RSpec::Core::RakeTask.new(:test).tap do |task|
|
5
|
+
# task.pattern = FileList['test/**{,/*/**}/*.rb'].exclude('test/**{,/*/**}/*helper.rb')
|
6
|
+
task.pattern = 'test/**{,/*/**}/*.rb'
|
7
|
+
end
|
5
8
|
|
6
|
-
task :default => :
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
task :temp do
|
12
|
+
require 'clips'
|
13
|
+
clips = CLIPS.new
|
14
|
+
# clips.add 'rule1', CLIPS::Rule.new(:foo, :actions => :bar)
|
15
|
+
clips.add :foo
|
16
|
+
|
17
|
+
# rule2 = CLIPS::Rule.new(:bar) do
|
18
|
+
# rule2 = CLIPS::Rule.new(:foo) { puts "Rule 2!" }
|
19
|
+
clips.add :rule2, CLIPS::Rule.new(:foo) { puts "Rule 2!" }
|
20
|
+
|
21
|
+
puts clips.run # => "Rule 2!"
|
22
|
+
p clips.facts # => #<Set: {[:foo], [:bar]}>
|
23
|
+
end
|
data/clips.gemspec
CHANGED
@@ -2,23 +2,24 @@ lib = File.expand_path("../lib", __FILE__)
|
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
|
4
4
|
Gem::Specification.new do |spec|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
spec.name = "clips"
|
6
|
+
spec.version = '0.1'
|
7
|
+
spec.authors = ["Brandon Fosdick"]
|
8
|
+
spec.email = ["bfoz@bfoz.net"]
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
spec.summary = %q{The CLIPS engine reimplemented in Ruby}
|
11
|
+
spec.description = %q{The C Language Integrated Production System (CLIPS), but in Ruby}
|
12
|
+
spec.homepage = "http://github.com/bfoz/clips-ruby"
|
13
|
+
spec.license = "BSD"
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "bin"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
24
25
|
end
|
data/lib/clips.rb
CHANGED
@@ -1,5 +1,121 @@
|
|
1
|
-
require
|
1
|
+
require 'set'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative 'clips/rule'
|
4
|
+
|
5
|
+
class CLIPS
|
6
|
+
# @return [Set] The currently activated Facts awaiting processing
|
7
|
+
attr_reader :activations
|
8
|
+
|
9
|
+
# @return [Set] The Rules waiting to be processed
|
10
|
+
attr_reader :agenda
|
11
|
+
|
12
|
+
# @return [Hash] The set of facts that will be instantiated on each reset
|
13
|
+
attr_reader :default_facts
|
14
|
+
|
15
|
+
# @return [Set] Just the Facts
|
16
|
+
attr_reader :facts
|
17
|
+
|
18
|
+
# @return [Hash] The set of Rules and their names
|
19
|
+
attr_reader :rules
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@activations = Set.new
|
23
|
+
@agenda = Set.new
|
24
|
+
@default_facts = Hash.new
|
25
|
+
@facts = Set.new
|
26
|
+
@rules = Hash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# @overload add(name, rule)
|
30
|
+
# Add the named {Rule}
|
31
|
+
# @param [String] name The name of the {Rule}
|
32
|
+
# @param [Rule] rule The {Rule} to add
|
33
|
+
# @return [CLIPS]
|
34
|
+
# @overload add(*fields)
|
35
|
+
# Add a fact using the given field values
|
36
|
+
# @return [CLIPS]
|
37
|
+
def add(*fields)
|
38
|
+
if (2==fields.length) and fields.last&.is_a?(Rule)
|
39
|
+
self.rules[fields.first.to_sym] ||= fields.last
|
40
|
+
else # Assume the item to be a Fact
|
41
|
+
fields[0] = fields.first.to_sym
|
42
|
+
# Add the new Fact to the activations list for processing during the next call to run()
|
43
|
+
self.activations.add(fields)
|
44
|
+
self.facts.add(fields)
|
45
|
+
fields
|
46
|
+
# else
|
47
|
+
# raise ArgumentError.new("Invalid fact")
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove all Facts and return self
|
53
|
+
# @return [CLIPS]
|
54
|
+
def clear
|
55
|
+
self.activations.clear
|
56
|
+
self.agenda.clear
|
57
|
+
self.default_facts.clear
|
58
|
+
self.facts.clear
|
59
|
+
self.rules.clear
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Remove the given fact
|
64
|
+
# @return [CLIPS]
|
65
|
+
def delete(*fact)
|
66
|
+
fact[0] = fact.first.to_sym
|
67
|
+
if self.facts.delete?(fact)
|
68
|
+
self.activations.delete(fact)
|
69
|
+
end
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Delete everything but the rules, then add the default facts
|
74
|
+
# @return [CLIPS] self
|
75
|
+
def reset
|
76
|
+
self.activations.clear
|
77
|
+
self.agenda.clear
|
78
|
+
self.facts.clear
|
79
|
+
|
80
|
+
self.default_facts.each do |name, fact_set|
|
81
|
+
fact_set.each {|fact| self.add(*fact)}
|
82
|
+
end
|
83
|
+
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param [Integer] limit The maximum number of rules that will fire before the function returns. The limit is disabled if nil.
|
88
|
+
# @return [Integer] The number of rules that were fired
|
89
|
+
def run(limit=nil)
|
90
|
+
self.rules.each do |name, rule|
|
91
|
+
self.agenda.add(rule) if rule.patterns.empty?
|
92
|
+
end
|
93
|
+
|
94
|
+
rule_counter = 0
|
95
|
+
begin
|
96
|
+
# If all of a Rule's patterns match, and any of those patterns are on the activation list, then fire the Rule
|
97
|
+
self.rules.each do |name, rule|
|
98
|
+
activated = false
|
99
|
+
_all = rule.patterns.all? do |pattern|
|
100
|
+
activated = true if not activated and self.activations.include?(pattern)
|
101
|
+
self.facts.include?(pattern)
|
102
|
+
end
|
103
|
+
self.agenda.add(rule) if activated and _all
|
104
|
+
end
|
105
|
+
|
106
|
+
self.activations.clear # All activations have been processed, so clear the Set
|
107
|
+
|
108
|
+
# Fire all of the Rules on the agenda in salience-order
|
109
|
+
sorted_agenda = self.agenda.sort {|a,b| b.salience <=> a.salience}
|
110
|
+
self.agenda.clear
|
111
|
+
sorted_agenda.each do |rule|
|
112
|
+
rule.run!(self)
|
113
|
+
rule_counter += 1
|
114
|
+
|
115
|
+
break if rule_counter == limit
|
116
|
+
end
|
117
|
+
end until self.agenda.empty? and self.activations.empty?
|
118
|
+
|
119
|
+
rule_counter
|
120
|
+
end
|
5
121
|
end
|
data/lib/clips/rule.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class CLIPS; end
|
2
|
+
|
3
|
+
class CLIPS::Rule
|
4
|
+
attr_reader :actions
|
5
|
+
attr_reader :block
|
6
|
+
attr_reader :patterns
|
7
|
+
attr_reader :salience
|
8
|
+
|
9
|
+
def initialize(*args, actions:[], patterns:[], salience:0, &block)
|
10
|
+
@actions = [actions].flatten(1)
|
11
|
+
@block = block
|
12
|
+
@patterns = patterns
|
13
|
+
@salience = salience
|
14
|
+
|
15
|
+
@patterns.push args unless args.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Assumes that the patterns have already been checked
|
19
|
+
# @param [CLIPS] environment The thing with all of the facts
|
20
|
+
# @param [Array] patterns The matching patterns that triggered this {Rule}
|
21
|
+
def run!(environment, *patterns)
|
22
|
+
if @block
|
23
|
+
@block.call(*patterns)
|
24
|
+
else
|
25
|
+
self.actions.each do |action|
|
26
|
+
environment.add(action)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clips
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0'
|
4
|
+
version: '0.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Fosdick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -69,8 +69,10 @@ files:
|
|
69
69
|
- bin/setup
|
70
70
|
- clips.gemspec
|
71
71
|
- lib/clips.rb
|
72
|
+
- lib/clips/rule.rb
|
72
73
|
homepage: http://github.com/bfoz/clips-ruby
|
73
|
-
licenses:
|
74
|
+
licenses:
|
75
|
+
- BSD
|
74
76
|
metadata: {}
|
75
77
|
post_install_message:
|
76
78
|
rdoc_options: []
|
@@ -88,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
90
|
version: '0'
|
89
91
|
requirements: []
|
90
92
|
rubyforge_project:
|
91
|
-
rubygems_version: 2.
|
93
|
+
rubygems_version: 2.7.3
|
92
94
|
signing_key:
|
93
95
|
specification_version: 4
|
94
96
|
summary: The CLIPS engine reimplemented in Ruby
|