ruby-prolog 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +19 -0
- data/Manifest +10 -0
- data/README.txt +57 -0
- data/Rakefile +10 -0
- data/examples/acls.rb +77 -0
- data/examples/hanoi.rb +36 -0
- data/lib/ruby-prolog.rb +49 -0
- data/lib/ruby-prolog/ruby-prolog.rb +366 -0
- data/ruby-prolog.gemspec +31 -0
- data/spec/ruby-prolog_spec.rb +222 -0
- data/spec/spec_helper.rb +16 -0
- metadata +70 -0
data/History.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
== 0.0.4 / 2009-02-09
|
2
|
+
|
3
|
+
* Fixing packaging error from previous build.
|
4
|
+
|
5
|
+
== 0.0.3 / 2009-02-09
|
6
|
+
|
7
|
+
* Renaming files due to RubyForge stupidity.
|
8
|
+
* Adding usage examples.
|
9
|
+
|
10
|
+
== 0.0.2 / 2009-02-04
|
11
|
+
|
12
|
+
* Refactored the code into an object-oriented state.
|
13
|
+
* Changed #query to not print results and return goals.
|
14
|
+
* Made spec tests more meaningful.
|
15
|
+
* Addressed small TODO items.
|
16
|
+
|
17
|
+
== 0.0.1 / 2009-01-14
|
18
|
+
|
19
|
+
* First release. Basically a glorified packaging of tiny_prolog and various extensions.
|
data/Manifest
ADDED
data/README.txt
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
ruby_prolog
|
2
|
+
by Preston Lee
|
3
|
+
http://openrain.com
|
4
|
+
The core engine is largely based on tiny_prolog, though numerous additional enhancements have been made
|
5
|
+
such as object-oriented refactorings and integration of ideas from the interwebs. Unfortunately I cannot
|
6
|
+
read Japanese and cannot give proper attribution to the original tiny_prolog author. If *you* can, let
|
7
|
+
me know and I'll update this document!
|
8
|
+
|
9
|
+
== DESCRIPTION:
|
10
|
+
|
11
|
+
An object-oriented pure Ruby implementation of a Prolog-like DSL for easy AI and logical programming.
|
12
|
+
|
13
|
+
== FEATURES/PROBLEMS:
|
14
|
+
|
15
|
+
* Pure Ruby.
|
16
|
+
* Tested with Ruby 1.8.7 (MRI).
|
17
|
+
* Object-oriented.
|
18
|
+
* Multiple Prolog environments can be created and manipulated simultaneously.
|
19
|
+
* Concurrent access to different core instances should be safe.
|
20
|
+
* Concurrent access to a single core instance might probably explode in odd ways.
|
21
|
+
|
22
|
+
== SYNOPSIS:
|
23
|
+
|
24
|
+
See ruby_prolog_spec.rb for usage examples.
|
25
|
+
|
26
|
+
== REQUIREMENTS:
|
27
|
+
|
28
|
+
* Should work under all popular Ruby interpreters. Please report compatibility problems.
|
29
|
+
|
30
|
+
== INSTALL:
|
31
|
+
|
32
|
+
* sudo gem install ruby_prolog
|
33
|
+
|
34
|
+
== LICENSE:
|
35
|
+
|
36
|
+
(The MIT License)
|
37
|
+
|
38
|
+
Copyright (c) 2008 OpenRain, LLC
|
39
|
+
|
40
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
41
|
+
a copy of this software and associated documentation files (the
|
42
|
+
'Software'), to deal in the Software without restriction, including
|
43
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
44
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
45
|
+
permit persons to whom the Software is furnished to do so, subject to
|
46
|
+
the following conditions:
|
47
|
+
|
48
|
+
The above copyright notice and this permission notice shall be
|
49
|
+
included in all copies or substantial portions of the Software.
|
50
|
+
|
51
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
52
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
53
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
54
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
55
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
56
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
57
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'echoe'
|
2
|
+
Echoe.new("ruby-prolog") do |p|
|
3
|
+
p.author = "Preston Lee"
|
4
|
+
p.summary = "A Prolog-ish Ruby DSL."
|
5
|
+
p.url = "http://www.openrain.com.com"
|
6
|
+
# p.docs_host = "uncapitalizer.com:~/www/files/doc/"
|
7
|
+
# p.runtime_dependencies = ["string_tools >=1.4.0"]
|
8
|
+
p.version = '0.0.5'
|
9
|
+
p.email = "preston.lee@openrain.com"
|
10
|
+
end
|
data/examples/acls.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ruby-prolog'
|
3
|
+
|
4
|
+
c = RubyProlog::Core.new
|
5
|
+
c.instance_eval do
|
6
|
+
|
7
|
+
# Let's put together an issue tracking system for Microsoft.
|
8
|
+
|
9
|
+
# We'll start by declaring a few projects...
|
10
|
+
project_status['me', 'live'].fact
|
11
|
+
project_status['xp', 'live'].fact
|
12
|
+
project_status['vista', 'live'].fact
|
13
|
+
project_status['7', 'in_progress'].fact
|
14
|
+
|
15
|
+
|
16
|
+
# Now we'll create a custom ACL system...
|
17
|
+
role_can['user', 'create'].fact
|
18
|
+
role_can['user', 'read'].fact
|
19
|
+
|
20
|
+
role_can['qa', 'create'].fact
|
21
|
+
role_can['qa', 'read'].fact
|
22
|
+
role_can['qa', 'update'].fact
|
23
|
+
|
24
|
+
role_can['admin', 'create'].fact
|
25
|
+
role_can['admin', 'read'].fact
|
26
|
+
role_can['admin', 'update'].fact
|
27
|
+
role_can['admin', 'delete'].fact
|
28
|
+
|
29
|
+
|
30
|
+
# Let's put people on different projects
|
31
|
+
assigned['alice', 'me', 'user'].fact
|
32
|
+
assigned['bob', 'me', 'qa'].fact
|
33
|
+
assigned['charlie', 'me', 'qa'].fact
|
34
|
+
|
35
|
+
assigned['alice', 'xp', 'user'].fact
|
36
|
+
assigned['bob', 'xp', 'user'].fact
|
37
|
+
assigned['charlie', 'xp', 'admin'].fact
|
38
|
+
|
39
|
+
assigned['alice', 'vista', 'qa'].fact
|
40
|
+
assigned['bob', 'vista', 'admin'].fact
|
41
|
+
assigned['charlie', 'vista', 'admin'].fact
|
42
|
+
|
43
|
+
assigned['alice', '7', 'user'].fact
|
44
|
+
assigned['bob', '7', 'qa'].fact
|
45
|
+
assigned['charlie', '7', 'qa'].fact
|
46
|
+
assigned['dale', '7', 'admin'].fact
|
47
|
+
|
48
|
+
|
49
|
+
# can_read_on_project[:U, :P] <<= [assigned[:U, :P, :R], role_can[:R, 'read']]
|
50
|
+
can_on_project[:U, :X, :P] <<= [assigned[:U, :P, :R], role_can[:R, :X]]
|
51
|
+
is_role_on_multiple_projects[:U, :R] <<= [assigned[:U, :X, :R], assigned[:U, :Y, :R], noteq[:X, :Y]]
|
52
|
+
# , noteq[:P1, :P2]
|
53
|
+
|
54
|
+
puts 'Who does QA?'
|
55
|
+
p query(assigned[:U, :P, 'qa'])
|
56
|
+
|
57
|
+
puts "Who can access the 'vista' project?"
|
58
|
+
p query(can_on_project[:U, 'read', 'vista'])
|
59
|
+
|
60
|
+
puts "Does Alice have delete privileges on Vista?"
|
61
|
+
puts query(can_on_project['alice', 'delete', 'vista']).empty? ? "Yes" : "No"
|
62
|
+
|
63
|
+
puts "Does Bob have delete privileges on Vista?"
|
64
|
+
puts query(can_on_project['bob', 'delete', 'vista']).empty? ? "Yes" : "No"
|
65
|
+
|
66
|
+
puts "Who is an admin on multiple projects?"
|
67
|
+
# p query(is_role_on_multiple_projects[:U, 'admin'])
|
68
|
+
|
69
|
+
s = Array.new
|
70
|
+
query(is_role_on_multiple_projects[:U, 'admin']).each do |r|
|
71
|
+
s |= [r[0].args[0]] # Put each result into the array, if not already present.
|
72
|
+
end
|
73
|
+
s.each do |n| puts n end # Print all unique results!
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
end
|
data/examples/hanoi.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ruby-prolog'
|
3
|
+
|
4
|
+
# require File.join(File.dirname(__FILE__), %w[.. lib ruby-prolog])
|
5
|
+
|
6
|
+
# Inspired by..
|
7
|
+
# http://www.csupomona.edu/~jrfisher/www/prolog_tutorial/2_3.html
|
8
|
+
# http://eigenclass.org/hiki.rb?tiny+prolog+in+ruby
|
9
|
+
|
10
|
+
c = RubyProlog::Core.new
|
11
|
+
c.instance_eval do
|
12
|
+
|
13
|
+
move[0,:X,:Y,:Z] <<= :CUT # There are no more moves left
|
14
|
+
move[:N,:A,:B,:C] <<= [
|
15
|
+
is(:M,:N){|n| n - 1}, # reads as "M IS N - 1"
|
16
|
+
move[:M,:A,:C,:B],
|
17
|
+
write_info[:A,:B],
|
18
|
+
move[:M,:C,:B,:A]
|
19
|
+
]
|
20
|
+
write_info[:X,:Y] <<= [
|
21
|
+
write["move a disc from the "],
|
22
|
+
write[:X], write[" pole to the "],
|
23
|
+
write[:Y], writenl[" pole "]
|
24
|
+
]
|
25
|
+
|
26
|
+
hanoi[:N] <<= move[:N,"left","right","center"]
|
27
|
+
|
28
|
+
puts "\nWhat's the solution for a single disc?"
|
29
|
+
query(hanoi[1])
|
30
|
+
|
31
|
+
puts "\n\nWhat's the solution for 5 discs?"
|
32
|
+
query(hanoi[5])
|
33
|
+
|
34
|
+
# do_stuff[:STUFF].calls{|env| print env[:STUFF]; true}
|
35
|
+
|
36
|
+
end
|
data/lib/ruby-prolog.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
module RubyProlog
|
3
|
+
|
4
|
+
# :stopdoc:
|
5
|
+
VERSION = '0.0.5'
|
6
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
7
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
8
|
+
# :startdoc:
|
9
|
+
|
10
|
+
# Returns the version string for the library.
|
11
|
+
#
|
12
|
+
def self.version
|
13
|
+
VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the library path for the module. If any arguments are given,
|
17
|
+
# they will be joined to the end of the libray path using
|
18
|
+
# <tt>File.join</tt>.
|
19
|
+
#
|
20
|
+
def self.libpath( *args )
|
21
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the lpath for the module. If any arguments are given,
|
25
|
+
# they will be joined to the end of the path using
|
26
|
+
# <tt>File.join</tt>.
|
27
|
+
#
|
28
|
+
def self.path( *args )
|
29
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Utility method used to rquire all files ending in .rb that lie in the
|
33
|
+
# directory below this file that has the same name as the filename passed
|
34
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
35
|
+
# the _filename_ does not have to be equivalent to the directory.
|
36
|
+
#
|
37
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
38
|
+
dir ||= ::File.basename(fname, '.*')
|
39
|
+
search_me = ::File.expand_path(
|
40
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
41
|
+
|
42
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
43
|
+
end
|
44
|
+
|
45
|
+
end # module RubyProlog
|
46
|
+
|
47
|
+
RubyProlog.require_all_libs_relative_to(__FILE__)
|
48
|
+
|
49
|
+
# EOF
|
@@ -0,0 +1,366 @@
|
|
1
|
+
# Based on tiny_prolog h18.9/8
|
2
|
+
|
3
|
+
module RubyProlog
|
4
|
+
|
5
|
+
|
6
|
+
class Predicate
|
7
|
+
|
8
|
+
attr_reader :defs
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
@defs = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
return @name.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](*args)
|
20
|
+
return Goal.new(self, args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(*a); end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
class Goal
|
29
|
+
|
30
|
+
attr_reader :pred, :args
|
31
|
+
|
32
|
+
def list(*x)
|
33
|
+
y = nil
|
34
|
+
x.reverse_each {|e| y = Cons.new(e, y)}
|
35
|
+
return y
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(pred, args)
|
39
|
+
@pred, @args = pred, args
|
40
|
+
end
|
41
|
+
|
42
|
+
def si(*rhs) # ラテン語の「もしも」
|
43
|
+
@pred.defs << [self, list(*rhs)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def fact
|
47
|
+
si
|
48
|
+
end
|
49
|
+
|
50
|
+
def <<(rhs)
|
51
|
+
case rhs
|
52
|
+
when Array
|
53
|
+
si(*rhs)
|
54
|
+
else
|
55
|
+
si(rhs)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def calls(&callback)
|
60
|
+
@pred.defs << [self, callback]
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
return @pred.inspect.to_s + @args.inspect.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Lisp のリスト風の二項組
|
71
|
+
class Cons < Array
|
72
|
+
|
73
|
+
def initialize(car, cdr)
|
74
|
+
super(2)
|
75
|
+
self[0], self[1] = car, cdr
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
repr = proc {|x|
|
80
|
+
car, cdr = x[0], x[1]
|
81
|
+
if cdr.nil? then [car.inspect]
|
82
|
+
elsif Cons === cdr then repr[cdr].unshift(car.inspect)
|
83
|
+
else [car.inspect, '.', cdr.inspect]
|
84
|
+
end
|
85
|
+
}
|
86
|
+
return '(' + repr[self].join(' ') + ')'
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
class Environment
|
94
|
+
|
95
|
+
def initialize
|
96
|
+
@table = {}
|
97
|
+
end
|
98
|
+
|
99
|
+
def put(x, pair)
|
100
|
+
@table[x] = pair
|
101
|
+
end
|
102
|
+
|
103
|
+
def get(x)
|
104
|
+
return @table[x]
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete(x)
|
108
|
+
@table.delete(x) {|k| raise "#{k} not found in #{inspect}"}
|
109
|
+
end
|
110
|
+
|
111
|
+
def clear
|
112
|
+
@table.clear
|
113
|
+
end
|
114
|
+
|
115
|
+
def dereference(t)
|
116
|
+
env = self
|
117
|
+
while Symbol === t
|
118
|
+
p = env.get(t)
|
119
|
+
break if p.nil?
|
120
|
+
t, env = p
|
121
|
+
end
|
122
|
+
return [t, env]
|
123
|
+
end
|
124
|
+
|
125
|
+
def [](t)
|
126
|
+
t, env = dereference(t)
|
127
|
+
return case t
|
128
|
+
when Goal then Goal.new(t.pred, env[t.args])
|
129
|
+
when Cons then Cons.new(env[t[0]], env[t[1]])
|
130
|
+
when Array then t.collect {|e| env[e]}
|
131
|
+
else t
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
class CallbackEnvironment
|
140
|
+
|
141
|
+
def initialize(env, trail, core)
|
142
|
+
@env, @trail, @core = env, trail, core
|
143
|
+
end
|
144
|
+
|
145
|
+
def [](t)
|
146
|
+
return @env[t]
|
147
|
+
end
|
148
|
+
|
149
|
+
def unify(t, u)
|
150
|
+
# pp "CORE " + @core
|
151
|
+
return @core._unify(t, @env, u, @env, @trail, @env)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
class Core
|
158
|
+
|
159
|
+
def _unify(x, x_env, y, y_env, trail, tmp_env)
|
160
|
+
|
161
|
+
loop {
|
162
|
+
if Symbol === x
|
163
|
+
xp = x_env.get(x)
|
164
|
+
if xp.nil?
|
165
|
+
y, y_env = y_env.dereference(y)
|
166
|
+
unless x == y and x_env == y_env
|
167
|
+
x_env.put(x, [y, y_env])
|
168
|
+
trail << [x, x_env] unless x_env == tmp_env
|
169
|
+
end
|
170
|
+
return true
|
171
|
+
else
|
172
|
+
x, x_env = xp
|
173
|
+
x, x_env = x_env.dereference(x)
|
174
|
+
end
|
175
|
+
elsif Symbol === y
|
176
|
+
x, x_env, y, y_env = y, y_env, x, x_env
|
177
|
+
else
|
178
|
+
break
|
179
|
+
end
|
180
|
+
}
|
181
|
+
|
182
|
+
if Goal === x and Goal === y
|
183
|
+
return false unless x.pred == y.pred
|
184
|
+
x, y = x.args, y.args
|
185
|
+
end
|
186
|
+
|
187
|
+
if Array === x and Array === y
|
188
|
+
return false unless x.length == y.length
|
189
|
+
for i in 0 ... x.length # x.each_index do |i| も可
|
190
|
+
return false unless _unify(x[i], x_env, y[i], y_env, trail, tmp_env)
|
191
|
+
end
|
192
|
+
return true
|
193
|
+
else
|
194
|
+
return x == y
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
def list(*x)
|
201
|
+
y = nil
|
202
|
+
x.reverse_each {|e| y = Cons.new(e, y)}
|
203
|
+
return y
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
def resolve(*goals)
|
208
|
+
env = Environment.new
|
209
|
+
_resolve_body(list(*goals), env, [false]) {
|
210
|
+
yield env
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
def _resolve_body(body, env, cut)
|
216
|
+
if body.nil?
|
217
|
+
yield
|
218
|
+
else
|
219
|
+
goal, rest = body
|
220
|
+
if goal == :CUT
|
221
|
+
_resolve_body(rest, env, cut) {
|
222
|
+
yield
|
223
|
+
}
|
224
|
+
cut[0] = true
|
225
|
+
else
|
226
|
+
d_env = Environment.new
|
227
|
+
d_cut = [false]
|
228
|
+
require 'pp'
|
229
|
+
# pp 'G ' + goal.class.to_s
|
230
|
+
# pp goal.pred
|
231
|
+
for d_head, d_body in goal.pred.defs
|
232
|
+
# for d_head, d_body in goal.defs
|
233
|
+
break if d_cut[0] or cut[0]
|
234
|
+
trail = []
|
235
|
+
if _unify_(goal, env, d_head, d_env, trail, d_env)
|
236
|
+
if Proc === d_body
|
237
|
+
if d_body[CallbackEnvironment.new(d_env, trail, self)]
|
238
|
+
_resolve_body(rest, env, cut) {
|
239
|
+
yield
|
240
|
+
}
|
241
|
+
end
|
242
|
+
else
|
243
|
+
_resolve_body(d_body, d_env, d_cut) {
|
244
|
+
_resolve_body(rest, env, cut) {
|
245
|
+
yield
|
246
|
+
}
|
247
|
+
d_cut[0] ||= cut[0]
|
248
|
+
}
|
249
|
+
end
|
250
|
+
end
|
251
|
+
for x, x_env in trail
|
252
|
+
x_env.delete(x)
|
253
|
+
end
|
254
|
+
d_env.clear
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
$_trace = false
|
262
|
+
def trace(flag)
|
263
|
+
$_trace = flag
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
def _unify_(x, x_env, y, y_env, trail, tmp_env)
|
268
|
+
lhs, rhs = x_env[x].inspect, y.inspect if $_trace
|
269
|
+
unified = _unify(x, x_env, y, y_env, trail, tmp_env)
|
270
|
+
printf("\t%s %s %s\n", lhs, (unified ? "~" : "!~"), rhs) if $_trace
|
271
|
+
return unified
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def query(*goals)
|
276
|
+
count = 0
|
277
|
+
results = Array.new
|
278
|
+
# printout = proc {|x|
|
279
|
+
# x = x[0] if x.length == 1
|
280
|
+
# printf "%d %s\n", count, x.inspect
|
281
|
+
# }
|
282
|
+
resolve(*goals) {|env|
|
283
|
+
count += 1
|
284
|
+
results << env[goals]
|
285
|
+
# printout[env[goals]]
|
286
|
+
}
|
287
|
+
# printout[goals] if count == 0
|
288
|
+
return results
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
def is(*syms,&block)
|
293
|
+
$is_cnt ||= 0
|
294
|
+
is = Predicate.new "IS_#{$is_cnt += 1}"
|
295
|
+
raise "At least one symbol needed" unless syms.size > 0
|
296
|
+
is[*syms].calls do |env|
|
297
|
+
value = block.call(*syms[1..-1].map{|x| env[x]})
|
298
|
+
env.unify(syms.first, value)
|
299
|
+
end
|
300
|
+
is[*syms]
|
301
|
+
end
|
302
|
+
|
303
|
+
def method_missing(meth, *args)
|
304
|
+
# puts "NEW PRED #{meth} #{meth.class}"
|
305
|
+
pred = Predicate.new(meth)
|
306
|
+
# proc = Proc.new {pred}
|
307
|
+
|
308
|
+
|
309
|
+
# We only want to define the method on this specific object instance to avoid polluting global namespaces.
|
310
|
+
|
311
|
+
# You can't do this..
|
312
|
+
# class << self
|
313
|
+
# module_eval do
|
314
|
+
# send(:define_method, m, proc)
|
315
|
+
# end
|
316
|
+
# end
|
317
|
+
|
318
|
+
# Nor this..
|
319
|
+
# define_method(meth) {pred}
|
320
|
+
|
321
|
+
# Nor this..
|
322
|
+
# self.send(:define_method, meth, proc)
|
323
|
+
|
324
|
+
# And you don't want to pollute the global namespace like this...
|
325
|
+
# Object.class_eval{ define_method(meth){pr} }
|
326
|
+
|
327
|
+
|
328
|
+
# Sooooo... I know this doesn't really make intuitive sense,
|
329
|
+
# but you need to get the eigenclass and then define
|
330
|
+
# the method within that context in such a way that we
|
331
|
+
# have access to local variables, like this...
|
332
|
+
class << self; self; end.module_eval do
|
333
|
+
define_method meth, Proc.new{pred}
|
334
|
+
end
|
335
|
+
# ...which is major fuglytown, but I don't know how to do it any other way.
|
336
|
+
|
337
|
+
return pred
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
def initialize
|
342
|
+
# We do not need to predefine predicates like this because they will automatically be defined for us.
|
343
|
+
# write = Predicate.new "write"
|
344
|
+
write[:X].calls{|env| print env[:X]; true}
|
345
|
+
writenl[:X].calls{|env| puts env[:X]; true}
|
346
|
+
nl[:X].calls{|e| puts; true}
|
347
|
+
eq[:X,:Y].calls{|env| env.unify(env[:X], env[:Y])}
|
348
|
+
noteq[:X,:Y].calls{|env| env[:X] != env[:Y]}
|
349
|
+
atomic[:X].calls do |env|
|
350
|
+
case env[:X]
|
351
|
+
when Symbol, Predicate, Goal; false
|
352
|
+
else true
|
353
|
+
end
|
354
|
+
end
|
355
|
+
notatomic[:X].calls do |env|
|
356
|
+
case env[:X]
|
357
|
+
when Symbol, Predicate, Goal; true
|
358
|
+
else false
|
359
|
+
end
|
360
|
+
end
|
361
|
+
numeric[:X].calls{|env| Numeric === env[:X] }
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
data/ruby-prolog.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{ruby-prolog}
|
5
|
+
s.version = "0.0.5"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Preston Lee"]
|
9
|
+
s.date = %q{2009-02-09}
|
10
|
+
s.description = %q{A Prolog-ish Ruby DSL.}
|
11
|
+
s.email = %q{preston.lee@openrain.com}
|
12
|
+
s.extra_rdoc_files = ["lib/ruby-prolog/ruby-prolog.rb", "lib/ruby-prolog.rb", "README.txt"]
|
13
|
+
s.files = ["examples/acls.rb", "examples/hanoi.rb", "History.txt", "lib/ruby-prolog/ruby-prolog.rb", "lib/ruby-prolog.rb", "Manifest", "Rakefile", "README.txt", "spec/ruby-prolog_spec.rb", "spec/spec_helper.rb", "ruby-prolog.gemspec"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.homepage = %q{http://www.openrain.com.com}
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Ruby-prolog", "--main", "README.txt"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = %q{ruby-prolog}
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
20
|
+
s.summary = %q{A Prolog-ish Ruby DSL.}
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 2
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
27
|
+
else
|
28
|
+
end
|
29
|
+
else
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
3
|
+
|
4
|
+
|
5
|
+
describe RubyProlog do
|
6
|
+
|
7
|
+
|
8
|
+
# before :each do
|
9
|
+
# end
|
10
|
+
|
11
|
+
|
12
|
+
it 'should not pollute the global namespace with predicates.' do
|
13
|
+
|
14
|
+
# We'll create numerous instances of the engine and assert they do not interfere with each other.
|
15
|
+
one = RubyProlog::Core.new
|
16
|
+
one.instance_eval do
|
17
|
+
query(male[:X]).length.should == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
two = RubyProlog::Core.new
|
21
|
+
two.instance_eval do
|
22
|
+
male[:preston].fact
|
23
|
+
query(male[:X]).length.should == 1
|
24
|
+
end
|
25
|
+
|
26
|
+
three = RubyProlog::Core.new
|
27
|
+
three.instance_eval do
|
28
|
+
query(male[:X]).length.should == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
one.instance_eval do
|
32
|
+
query(male[:X]).length.should == 0
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
it 'should be able to query simple family trees.' do
|
40
|
+
|
41
|
+
c = RubyProlog::Core.new
|
42
|
+
c.instance_eval do
|
43
|
+
# Basic family tree relationships..
|
44
|
+
sibling[:X,:Y] <<= [ parent[:Z,:X], parent[:Z,:Y], noteq[:X,:Y] ]
|
45
|
+
mother[:X,:Y] <<= [parent[:X, :Y], female[:X]]
|
46
|
+
father[:X,:Y] <<= [parent[:X, :Y], male[:X]]
|
47
|
+
grandparent[:G,:C] <<= [ parent[:G,:P], parent[:P,:C]]
|
48
|
+
ancestor[:A, :C] <<= [parent[:A, :X], parent[:X, :B]]
|
49
|
+
mothers[:M, :C] <<= mother[:M, :C]
|
50
|
+
mothers[:M, :C] <<= [mother[:M, :X], mothers[:X, :C]]
|
51
|
+
fathers[:F, :C] <<= father[:F, :C]
|
52
|
+
fathers[:F, :C] <<= [father[:F, :X], fathers[:X, :C]]
|
53
|
+
widower[:W] <<= [married[:W, :X], deceased[:X], nl[deceased[:W]]]
|
54
|
+
widower[:W] <<= [married[:X, :W], deceased[:X], nl[deceased[:W]]]
|
55
|
+
|
56
|
+
# Basic parents relationships as could be stored in a typical relational database.
|
57
|
+
parent['Ms. Old', 'Marge'].fact
|
58
|
+
|
59
|
+
parent['Carol', 'Ron'].fact
|
60
|
+
parent['Kent', 'Ron'].fact
|
61
|
+
parent['Marge', 'Marcia'].fact
|
62
|
+
parent['Pappy', 'Marcia'].fact
|
63
|
+
|
64
|
+
parent['Marcia', 'Karen'].fact
|
65
|
+
parent['Marcia', 'Julie'].fact
|
66
|
+
parent['Ron', 'Karen'].fact
|
67
|
+
parent['Ron', 'Julie'].fact
|
68
|
+
|
69
|
+
parent['Matt', 'Silas'].fact
|
70
|
+
parent['Julie', 'Silas'].fact
|
71
|
+
parent['Preston', 'Cirrus'].fact # Technically our dog.. but whatever :)
|
72
|
+
parent['Karen', 'Cirrus'].fact
|
73
|
+
|
74
|
+
|
75
|
+
# Gender facts..
|
76
|
+
male['Preston'].fact
|
77
|
+
male['Kent'].fact
|
78
|
+
male['Pappy'].fact
|
79
|
+
male['Ron'].fact
|
80
|
+
male['Matt'].fact
|
81
|
+
female['Ms. Old'].fact
|
82
|
+
female['Carol'].fact
|
83
|
+
female['Marge'].fact
|
84
|
+
female['Marcia'].fact
|
85
|
+
female['Julie'].fact
|
86
|
+
female['Karen'].fact
|
87
|
+
|
88
|
+
|
89
|
+
# People die :(
|
90
|
+
deceased['Pappy'].fact
|
91
|
+
|
92
|
+
|
93
|
+
# Let's marry some people..
|
94
|
+
married['Carol', 'Kent'].fact
|
95
|
+
married['Marge', 'Pappy'].fact
|
96
|
+
married['Ron', 'Marcia'].fact
|
97
|
+
married['Matt', 'Julie'].fact
|
98
|
+
married['Preston', 'Karen'].fact
|
99
|
+
|
100
|
+
|
101
|
+
# And add some facts on personal interests..
|
102
|
+
interest['Karen', 'Music'].fact
|
103
|
+
interest['Karen', 'Movies'].fact
|
104
|
+
interest['Karen', 'Games'].fact
|
105
|
+
interest['Karen', 'Walks'].fact
|
106
|
+
interest['Preston', 'Music'].fact
|
107
|
+
interest['Preston', 'Movies'].fact
|
108
|
+
interest['Preston', 'Games'].fact
|
109
|
+
|
110
|
+
interest['Silas', 'Games'].fact
|
111
|
+
interest['Cirrus', 'Games'].fact
|
112
|
+
interest['Karen', 'Walks'].fact
|
113
|
+
interest['Ron', 'Walks'].fact
|
114
|
+
interest['Marcia', 'Walks'].fact
|
115
|
+
|
116
|
+
# Runs some queries..
|
117
|
+
|
118
|
+
# p "Who are Silas's parents?"
|
119
|
+
# Silas should have two parents: Matt and Julie.
|
120
|
+
r = query(parent[:P, 'Silas'])
|
121
|
+
r.length.should == 2
|
122
|
+
r[0][0].args[0].should == 'Matt'
|
123
|
+
r[1][0].args[0].should == 'Julie'
|
124
|
+
|
125
|
+
# p "Who is married?"
|
126
|
+
# We defined 5 married facts.
|
127
|
+
query(married[:A, :B]).length.should == 5
|
128
|
+
|
129
|
+
# p 'Are Karen and Julie siblings?'
|
130
|
+
# Yes, through two parents.
|
131
|
+
query(sibling['Karen', 'Julie']).length.should == 2
|
132
|
+
|
133
|
+
|
134
|
+
# p "Who likes to play games?"
|
135
|
+
# Four people.
|
136
|
+
query(interest[:X, 'Games']).length.should == 4
|
137
|
+
|
138
|
+
|
139
|
+
# p "Who likes to play checkers?"
|
140
|
+
# Nobody.
|
141
|
+
query(interest[:X, 'Checkers']).length.should == 0
|
142
|
+
|
143
|
+
# p "Who are Karen's ancestors?"
|
144
|
+
# query(ancestor[:A, 'Karen'])
|
145
|
+
|
146
|
+
# p "What grandparents are also widowers?"
|
147
|
+
# Marge, twice, because of two grandchildren.
|
148
|
+
query(widower[:X], grandparent[:X, :G]).length.should == 2
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
it 'should be able to query simple family trees.' do
|
155
|
+
|
156
|
+
c = RubyProlog::Core.new
|
157
|
+
c.instance_eval do
|
158
|
+
|
159
|
+
vendor['dell'].fact
|
160
|
+
vendor['apple'].fact
|
161
|
+
|
162
|
+
model['ultrasharp'].fact
|
163
|
+
model['xps'].fact
|
164
|
+
model['macbook'].fact
|
165
|
+
model['iphone'].fact
|
166
|
+
|
167
|
+
manufactures['dell', 'ultrasharp'].fact
|
168
|
+
manufactures['dell', 'xps'].fact
|
169
|
+
manufactures['apple', 'macbook'].fact
|
170
|
+
manufactures['apple', 'iphone'].fact
|
171
|
+
|
172
|
+
is_a['xps', 'laptop'].fact
|
173
|
+
is_a['macbook', 'laptop'].fact
|
174
|
+
is_a['ultrasharp', 'monitor'].fact
|
175
|
+
is_a['iphone', 'phone'].fact
|
176
|
+
|
177
|
+
kind['laptop']
|
178
|
+
kind['monitor']
|
179
|
+
kind['phone']
|
180
|
+
|
181
|
+
model[:M] <<= [manfactures[:V, :M]]
|
182
|
+
|
183
|
+
vendor_of[:V, :K] <<= [vendor[:V], manufactures[:V, :M], is_a[:M, :K]]
|
184
|
+
# not_vendor_of[:V, :K] <<= [vendor[:V], nl[vendor_of[:V, :K]]]
|
185
|
+
|
186
|
+
query(is_a[:K, 'laptop']).length == 2
|
187
|
+
query(vendor_of[:V, 'phone']) == 1
|
188
|
+
# pp query(not_vendor_of[:V, 'phone'])
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
it 'should solve the Towers of Hanoi problem.' do
|
195
|
+
c = RubyProlog::Core.new
|
196
|
+
c.instance_eval do
|
197
|
+
|
198
|
+
move[0,:X,:Y,:Z] <<= :CUT # There are no more moves left
|
199
|
+
move[:N,:A,:B,:C] <<= [
|
200
|
+
is(:M,:N){|n| n - 1}, # reads as "M IS N - 1"
|
201
|
+
move[:M,:A,:C,:B],
|
202
|
+
# write_info[:A,:B],
|
203
|
+
move[:M,:C,:B,:A]
|
204
|
+
]
|
205
|
+
write_info[:X,:Y] <<= [
|
206
|
+
# write["move a disc from the "],
|
207
|
+
# write[:X], write[" pole to the "],
|
208
|
+
# write[:Y], writenl[" pole "]
|
209
|
+
]
|
210
|
+
|
211
|
+
hanoi[:N] <<= move[:N,"left","right","center"]
|
212
|
+
query(hanoi[5]).length.should == 1
|
213
|
+
|
214
|
+
# do_stuff[:STUFF].calls{|env| print env[:STUFF]; true}
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
|
222
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib ruby-prolog]))
|
4
|
+
|
5
|
+
Spec::Runner.configure do |config|
|
6
|
+
# == Mock Framework
|
7
|
+
#
|
8
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
9
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
10
|
+
#
|
11
|
+
# config.mock_with :mocha
|
12
|
+
# config.mock_with :flexmock
|
13
|
+
# config.mock_with :rr
|
14
|
+
end
|
15
|
+
|
16
|
+
# EOF
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-prolog
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Preston Lee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-09 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A Prolog-ish Ruby DSL.
|
17
|
+
email: preston.lee@openrain.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- lib/ruby-prolog/ruby-prolog.rb
|
24
|
+
- lib/ruby-prolog.rb
|
25
|
+
- README.txt
|
26
|
+
files:
|
27
|
+
- examples/acls.rb
|
28
|
+
- examples/hanoi.rb
|
29
|
+
- History.txt
|
30
|
+
- lib/ruby-prolog/ruby-prolog.rb
|
31
|
+
- lib/ruby-prolog.rb
|
32
|
+
- Manifest
|
33
|
+
- Rakefile
|
34
|
+
- README.txt
|
35
|
+
- spec/ruby-prolog_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
- ruby-prolog.gemspec
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://www.openrain.com.com
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --line-numbers
|
43
|
+
- --inline-source
|
44
|
+
- --title
|
45
|
+
- Ruby-prolog
|
46
|
+
- --main
|
47
|
+
- README.txt
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "1.2"
|
61
|
+
version:
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project: ruby-prolog
|
65
|
+
rubygems_version: 1.3.1
|
66
|
+
signing_key:
|
67
|
+
specification_version: 2
|
68
|
+
summary: A Prolog-ish Ruby DSL.
|
69
|
+
test_files: []
|
70
|
+
|