ruby-prolog 0.0.5
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/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
|
+
|