ruby-prolog 1.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 95ccd423807966099f72796cd208b6f6f3a67e04
4
- data.tar.gz: e7466c847b517b701e7af1e7ed0d3a475e0120fb
2
+ SHA256:
3
+ metadata.gz: ffa39b815b60de57f1865b1c5b2abd00345623c2d90a81c4185f5800bfe957ca
4
+ data.tar.gz: 2b279f1a9c1e6cfb9320de5c52a6ee484f47e62f1001a58d3bd17c56ff9fca57
5
5
  SHA512:
6
- metadata.gz: 12038b878dd73582ed9dd113145f87a1275a1b6ce754ce8c7a802c4a68f0e3eb44e6fc3b0c91d0ab167fa929155b3bf8801ea671fb12d1ada5caa4f3b9d05f1f
7
- data.tar.gz: bbaa1e1e15f8a851760b82178fa2b19ff66a68f0d1b0e7c57a9b74e9762415c6f6651fcb30f1dbd2e136689e8161c6f5f17c25df85373834f71503d5772399b3
6
+ metadata.gz: 6efaec42888b2749232637261b933e3a07c0a889b4ebc73995b7252e02048438b74b11d9c55a24e2687ecb0581e6d3c6d1efaad43444e3fa4601b5bc9165c432
7
+ data.tar.gz: f73024546d7bfd77b39fbd738bd06eacb0b1b3f025309fc55b3a43e332e7b2b8c8ab714db399886a62003a1b261ceab2ed65ee5d7adff9ac3cb267a399a864de
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
1
  source 'https://rubygems.org'
2
- ruby '2.0.0'
2
+ ruby '~> 2.0'
3
3
 
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'minitest-focus', '~> 1.1.2'
8
+ end
data/README.md CHANGED
@@ -1,50 +1,134 @@
1
- ruby_prolog
1
+ ruby-prolog
2
2
  ====
3
3
 
4
- An object-oriented pure Ruby implementation of a Prolog-like DSL for easy AI and logical programming. It should work under all popular Ruby interpreters. Please report compatibility problems.
4
+ ruby-prolog allows you to solve complex logic problems on the fly using a dynamic, Prolog-like DSL inline with your normal Ruby code. Basic use is encompassed by stating basic facts using your data, defining rules, and then asking questions. Why is this cool? Because ruby-prolog allows you to leave your normal object-oriented vortex on demand and step into the alternate reality of declarative languages.
5
5
 
6
- The core engine is largely based on tiny_prolog, though numerous additional enhancements have been made
7
- such as object-oriented refactorings and integration of ideas from the interwebs. Unfortunately I cannot
8
- read Japanese and cannot give proper attribution to the original tiny_prolog author. (If *you* can, let
9
- me know and I'll update this document!)
6
+ With ruby-prolog:
10
7
 
8
+ * There are no classes.
9
+ * There are no functions.
10
+ * There are no variables.
11
+ * There are no control flow statements.
12
+
13
+ You *can* use all these wonder things -- it’s still Ruby after all -- but they’re not needed, and mainly useful for getting data and results into/out of the interpreter. Prolog still tends to be favored heavily in artificial intelligence and theorem proving applications and is still relevant to computer science curricula as well, so I hope this updated release proves useful for your logic evaluation needs!
14
+
15
+ ruby-prolog is written using object-oriented-ish pure Ruby, and should work under all most popular Ruby interpreters. Please report compatibility problems. The core engine is largely based on tiny_prolog, though numerous additional enhancements have been made such as object-oriented refactorings and integration of ideas from the interwebs. Unfortunately I cannot read Japanese and cannot give proper attribution to the original tiny_prolog author. (If *you* can, let me know and I'll update this document!)
11
16
 
12
17
  Usage
13
18
  ----
14
19
 
15
- Two runnable examples are included in the 'bin' directory. The first..
20
+ Say you want to write the following Prolog code:
16
21
 
17
- ruby-prolog-hanoi
22
+ ```
23
+ implication(a, b).
24
+ implication(b, c).
25
+ implication(c, d).
26
+ implication(c, x).
27
+
28
+ implies(A, B) :- implication(A, B).
29
+ implies(A, B) :- implication(A, Something), implies(Something, B).
30
+ ```
31
+
32
+ Here's the equivalent Ruby code using this library:
33
+
34
+ ```rb
35
+ db = RubyProlog.new do
36
+ implication['a', 'b'].fact
37
+ implication['b', 'c'].fact
38
+ implication['c', 'd'].fact
39
+ implication['c', 'x'].fact
40
+
41
+ implies[:A, :B] << implication[:A, :B]
42
+ implies[:A, :B] << [
43
+ implication[:A, :Something],
44
+ implies[:Something, :B]
45
+ ]
46
+ end
47
+ ```
48
+
49
+ Now you can run some queries:
50
+
51
+ ```rb
52
+ # What are all the direct implications of 'c'?
53
+ db.query{ implication['c', :X] }
54
+ #=> [{ X: 'd' }, { X: 'x' }]
55
+
56
+ # What are all the things that can directly imply?
57
+ db.query{ implication[:X, :_] }
58
+ #=> [{ X: 'a' }, { X: 'b' }, { X: 'c' }, { X: 'c' }]
59
+
60
+ # What are all the things 'a' implies?
61
+ db.query{ implies['a', :X] }
62
+ #=> [{ X: 'b' }, { X: 'c' }, { X: 'd' }, { X: 'x' }]
63
+ ```
64
+
65
+ Unfortunately if you have **two** predicates in a query, you can't just use a comma. There two ways to solve this problem:
66
+
67
+ ```rb
68
+ # Solution 1: Use an array
69
+ db.query{[ implication['b', :S], implies[:S, :B] ]}
70
+
71
+ # Solution 2: Use a beneign assignment
72
+ db.query{_= implication['b', :S], implies[:S, :B] }
73
+ ```
74
+
75
+ If you need to add to your database, you can call `instance_eval`:
18
76
 
19
- ..is a ruby-prolog solution to the well-known "Towers of Hanoi" problem in computer science. The second..
77
+ ```rb
78
+ db = RubyProlog.new do
79
+ implication['a', 'b'].fact
80
+ implication['b', 'c'].fact
81
+ end
82
+
83
+ # Later...
84
+ db.instance_eval do
85
+ implication['c', 'd'].fact
86
+ implication['c', 'x'].fact
87
+ end
88
+ ```
89
+
90
+ This will mutate your database. If you want to "fork" your database instead, you can call `db.clone`, which will return a new instance with all stored data. Cloning like this is optimized to copy as little as possible.
91
+
92
+ Examples
93
+ ----
94
+
95
+ gem install ruby-prolog
96
+
97
+ Two runnable examples are included in the 'bin' directory. The first..
20
98
 
21
99
  ruby-prolog-acls
22
100
 
23
- ..shows the ruby-prolog DSL can be trivially used to implement an access control system. If you have some other useful or clever examples, please contribute them!
101
+ ..shows the ruby-prolog dynamic DSL used to trivially implement access control checks. The second..
24
102
 
25
103
 
104
+ ruby-prolog-hanoi
105
+
106
+ ..is a ruby-prolog solution to the well-known "Towers of Hanoi" problem in computer science. It's not clear, but something Prolog hackers will be interested in. If you have other useful or clever examples, please send a pull request!
107
+
108
+ See the test/ directory for additional examples.
109
+
26
110
  Features
27
111
  ----
28
112
 
29
113
  * Pure Ruby.
30
- * Tested with Ruby 2.0.0!
114
+ * No wacko dependencies.
115
+ * Tested with Ruby 2.0.0!
31
116
  * Object-oriented.
32
117
  * Multiple Prolog environments can be created and manipulated simultaneously.
33
118
  * Concurrent access to different core instances should be safe.
34
119
  * Concurrent access to a single core instance might probably explode in odd ways.
35
120
 
36
-
37
- Installation
121
+ Development
38
122
  ----
39
123
 
40
- gem install ruby_prolog
41
-
42
- See ruby_prolog_spec.rb for usage examples.
43
-
124
+ ```
125
+ $ git clone https://github.com/preston/ruby-prolog
126
+ $ cd ruby-prolog
127
+ $ bundle
128
+ $ rake test
129
+ ```
44
130
 
45
131
  License
46
132
  ----
47
133
 
48
- Released under the Apache 2 license.
49
-
50
- Copyright (c) 2013 Preston Lee. All rights reserved. http://prestonlee.com
134
+ Released under the Apache 2 license. Copyright (c) 2013 Preston Lee. All rights reserved. http://prestonlee.com
@@ -51,9 +51,9 @@ c.instance_eval do
51
51
  assigned['dale', '7', 'admin'].fact
52
52
 
53
53
 
54
- # can_read_on_project[:U, :P] <<= [assigned[:U, :P, :R], role_can[:R, 'read']]
55
- can_on_project[:U, :X, :P] <<= [assigned[:U, :P, :R], role_can[:R, :X]]
56
- is_role_on_multiple_projects[:U, :R] <<= [
54
+ # can_read_on_project[:U, :P] << [assigned[:U, :P, :R], role_can[:R, 'read']]
55
+ can_on_project[:U, :X, :P] << [assigned[:U, :P, :R], role_can[:R, :X]]
56
+ is_role_on_multiple_projects[:U, :R] << [
57
57
  assigned[:U, :X, :R],
58
58
  assigned[:U, :Y, :R],
59
59
  noteq[:X, :Y]]
@@ -82,4 +82,4 @@ c.instance_eval do
82
82
 
83
83
 
84
84
 
85
- end
85
+ end
@@ -12,20 +12,20 @@ require 'ruby-prolog'
12
12
  c = RubyProlog::Core.new
13
13
  c.instance_eval do
14
14
 
15
- move[0,:X,:Y,:Z] <<= :CUT # There are no more moves left
16
- move[:N,:A,:B,:C] <<= [
15
+ move[0,:X,:Y,:Z] << :CUT # There are no more moves left
16
+ move[:N,:A,:B,:C] << [
17
17
  is(:M,:N){|n| n - 1}, # reads as "M IS N - 1"
18
18
  move[:M,:A,:C,:B],
19
19
  write_info[:A,:B],
20
20
  move[:M,:C,:B,:A]
21
21
  ]
22
- write_info[:X,:Y] <<= [
22
+ write_info[:X,:Y] << [
23
23
  write["move a disc from the "],
24
24
  write[:X], write[" pole to the "],
25
25
  write[:Y], writenl[" pole "]
26
26
  ]
27
27
 
28
- hanoi[:N] <<= move[:N,"left","right","center"]
28
+ hanoi[:N] << move[:N,"left","right","center"]
29
29
 
30
30
  puts "\nWhat's the solution for a single disc?"
31
31
  query(hanoi[1])
@@ -35,4 +35,4 @@ c.instance_eval do
35
35
 
36
36
  # do_stuff[:STUFF].calls{|env| print env[:STUFF]; true}
37
37
 
38
- end
38
+ end
@@ -2,14 +2,23 @@
2
2
  # Fuglied by Preston Lee.
3
3
  module RubyProlog
4
4
 
5
-
5
+ def self.new(&block)
6
+ c = Core.new
7
+ c.instance_eval(&block) if block_given?
8
+ c
9
+ end
10
+
6
11
  class Predicate
7
-
8
- attr_reader :defs
12
+ @@id_counter = 0
13
+
14
+ attr_reader :id, :name
15
+ attr_accessor :db, :clauses
9
16
 
10
- def initialize(name)
17
+ def initialize(db, name)
18
+ @id = (@@id_counter += 1)
19
+ @db = db
11
20
  @name = name
12
- @defs = []
21
+ @clauses = []
13
22
  end
14
23
 
15
24
  def inspect
@@ -17,30 +26,36 @@ module RubyProlog
17
26
  end
18
27
 
19
28
  def [](*args)
20
- return Goal.new(self, args)
29
+ return TempClause.new(@db, self, args)
21
30
  end
22
31
 
23
- def []=(*a); end
24
-
25
- end
26
-
27
-
28
- class Goal
29
-
30
- attr_reader :pred, :args
32
+ def to_prolog
33
+ @clauses.map do |head, body|
34
+ "#{head.to_prolog}#{body ? " :- #{body.to_prolog}" : ''}."
35
+ end.join("\n")
36
+ end
31
37
 
32
- def list(*x)
33
- y = nil
34
- x.reverse_each {|e| y = Cons.new(e, y)}
35
- return y
38
+ def fork(new_db)
39
+ dupe = self.clone
40
+ dupe.db = new_db
41
+ dupe.clauses = dupe.clauses.dup
42
+ dupe
36
43
  end
37
-
38
- def initialize(pred, args)
39
- @pred, @args = pred, args
44
+ end
45
+
46
+ class TempClause
47
+ def initialize(db, pred, args)
48
+ @db, @pred, @args = db, pred, args
40
49
  end
41
50
 
42
51
  def si(*rhs)
43
- @pred.defs << [self, list(*rhs)]
52
+ goals = rhs.map do |x|
53
+ case x
54
+ when TempClause then x.to_goal
55
+ else x
56
+ end
57
+ end
58
+ @db.append(self.to_goal, list(*goals))
44
59
  end
45
60
 
46
61
  def fact
@@ -57,13 +72,69 @@ module RubyProlog
57
72
  end
58
73
 
59
74
  def calls(&callback)
60
- @pred.defs << [self, callback]
75
+ @db.append(self.to_goal, callback)
76
+ end
77
+
78
+ def to_goal
79
+ Goal.new(@pred.id, @pred.name, @args.map do |arg|
80
+ case arg
81
+ when TempClause
82
+ arg.to_goal
83
+ else
84
+ arg
85
+ end
86
+ end)
87
+ end
88
+
89
+ private
90
+
91
+ def list(*x)
92
+ y = nil
93
+ x.reverse_each {|e| y = Cons.new(e, y)}
94
+ return y
95
+ end
96
+ end
97
+
98
+ class Goal
99
+
100
+ attr_reader :pred_id, :pred_name, :args
101
+
102
+ def initialize(pred_id, pred_name, args)
103
+ @pred_id, @pred_name, @args = pred_id, pred_name, args
61
104
  end
62
105
 
63
106
  def inspect
64
- return @pred.inspect.to_s + @args.inspect.to_s
107
+ return @pred_name.to_s + @args.inspect.to_s
65
108
  end
66
109
 
110
+ def to_prolog
111
+ args_out = @args.map do |arg|
112
+ case arg
113
+ when Symbol
114
+ if arg == :_
115
+ "_"
116
+ elsif /[[:upper:]]/.match(arg.to_s[0])
117
+ arg.to_s
118
+ else
119
+ "_#{arg.to_s}"
120
+ end
121
+ when String
122
+ "'#{arg}'"
123
+ when Cons, Goal
124
+ arg.to_prolog
125
+ when Numeric
126
+ arg.to_s
127
+ else
128
+ raise "Unknown argument: #{arg.inspect}"
129
+ end
130
+ end.join(', ')
131
+
132
+ if @pred_name == :not_
133
+ "\\+ #{args_out}"
134
+ else
135
+ "#{@pred_name}(#{args_out})"
136
+ end
137
+ end
67
138
  end
68
139
 
69
140
 
@@ -86,6 +157,19 @@ module RubyProlog
86
157
  return '(' + repr[self].join(' ') + ')'
87
158
  end
88
159
 
160
+ def to_prolog
161
+ current = self
162
+ array = []
163
+ while current
164
+ array << case current[0]
165
+ when :CUT then '!'
166
+ when :_ then '_'
167
+ else current[0].to_prolog
168
+ end
169
+ current = current[1]
170
+ end
171
+ return array.join(', ')
172
+ end
89
173
  end
90
174
 
91
175
 
@@ -111,6 +195,24 @@ module RubyProlog
111
195
  @table.clear
112
196
  end
113
197
 
198
+ def solution
199
+ @table.map do |var, env|
200
+ xp = env
201
+ loop {
202
+ x, x_env = xp
203
+ y, y_env = x_env.dereference(x)
204
+ next_xp = y_env.get(x)
205
+ if next_xp.nil?
206
+ xp = [y, y_env]
207
+ break
208
+ else
209
+ xp = next_xp
210
+ end
211
+ }
212
+ [var, xp[0]]
213
+ end.to_h
214
+ end
215
+
114
216
  def dereference(t)
115
217
  env = self
116
218
  while Symbol === t
@@ -124,32 +226,79 @@ module RubyProlog
124
226
  def [](t)
125
227
  t, env = dereference(t)
126
228
  return case t
127
- when Goal then Goal.new(t.pred, env[t.args])
229
+ when Goal then Goal.new(t.pred_id, t.pred_name, env[t.args])
128
230
  when Cons then Cons.new(env[t[0]], env[t[1]])
129
231
  when Array then t.collect {|e| env[e]}
130
232
  else t
131
233
  end
132
234
  end
133
-
134
-
235
+
236
+
135
237
  end
136
238
 
137
239
 
138
240
  class CallbackEnvironment
139
-
241
+
140
242
  def initialize(env, trail, core)
141
243
  @env, @trail, @core = env, trail, core
142
244
  end
143
-
245
+
144
246
  def [](t)
145
247
  return @env[t]
146
248
  end
147
-
249
+
148
250
  def unify(t, u)
149
251
  # pp "CORE " + @core
150
252
  return @core._unify(t, @env, u, @env, @trail, @env)
151
253
  end
152
-
254
+
255
+ end
256
+
257
+
258
+ class Database
259
+ attr_reader :by_name, :by_id
260
+
261
+ def initialize
262
+ @by_name = {}
263
+ @by_id = {}
264
+ @listing_enabled = false
265
+ @listing = {}
266
+ end
267
+
268
+ def register(pred_name, skip_listing: false)
269
+ pred = @by_name[pred_name] = Predicate.new(self, pred_name)
270
+ @by_id[pred.id] = pred
271
+ @listing[pred.id] = false if skip_listing
272
+ pred
273
+ end
274
+
275
+ def enable_listing(flag=true)
276
+ @listing_enabled = true
277
+ end
278
+
279
+ def append(head, body)
280
+ pred = @by_id[head.pred_id]
281
+ if pred.nil?
282
+ raise "No such predicate for head: #{head.inspect}"
283
+ end
284
+ pred.clauses << [head, body]
285
+ if @listing_enabled && @listing[pred.id] != false
286
+ # Ruby hashes maintain insertion order
287
+ @listing[pred.id] = true
288
+ end
289
+ end
290
+
291
+ def initialize_copy(orig)
292
+ super
293
+ @by_id = @by_id.transform_values do |pred|
294
+ pred.fork(self)
295
+ end
296
+ @by_name = @by_name.transform_values {|pred| @by_id[pred.id]}
297
+ end
298
+
299
+ def listing
300
+ @listing.select{|_,v| v}.map{|k,v| @by_id[k]}
301
+ end
153
302
  end
154
303
 
155
304
 
@@ -158,7 +307,9 @@ module RubyProlog
158
307
  def _unify(x, x_env, y, y_env, trail, tmp_env)
159
308
 
160
309
  loop {
161
- if Symbol === x
310
+ if x == :_
311
+ return true
312
+ elsif Symbol === x
162
313
  xp = x_env.get(x)
163
314
  if xp.nil?
164
315
  y, y_env = y_env.dereference(y)
@@ -179,10 +330,10 @@ module RubyProlog
179
330
  }
180
331
 
181
332
  if Goal === x and Goal === y
182
- return false unless x.pred == y.pred
333
+ return false unless x.pred_id == y.pred_id
183
334
  x, y = x.args, y.args
184
335
  end
185
-
336
+
186
337
  if Array === x and Array === y
187
338
  return false unless x.length == y.length
188
339
  for i in 0 ... x.length # x.each_index do |i| も可
@@ -201,8 +352,8 @@ module RubyProlog
201
352
  x.reverse_each {|e| y = Cons.new(e, y)}
202
353
  return y
203
354
  end
204
-
205
-
355
+
356
+
206
357
  def resolve(*goals)
207
358
  env = Environment.new
208
359
  _resolve_body(list(*goals), env, [false]) {
@@ -224,11 +375,7 @@ module RubyProlog
224
375
  else
225
376
  d_env = Environment.new
226
377
  d_cut = [false]
227
- require 'pp'
228
- # pp 'G ' + goal.class.to_s
229
- # pp goal.pred
230
- for d_head, d_body in goal.pred.defs
231
- # for d_head, d_body in goal.defs
378
+ for d_head, d_body in @db.by_id[goal.pred_id].clauses
232
379
  break if d_cut[0] or cut[0]
233
380
  trail = []
234
381
  if _unify_(goal, env, d_head, d_env, trail, d_env)
@@ -271,26 +418,21 @@ module RubyProlog
271
418
  end
272
419
 
273
420
 
274
- def query(*goals)
275
- count = 0
276
- results = Array.new
277
- # printout = proc {|x|
278
- # x = x[0] if x.length == 1
279
- # printf "%d %s\n", count, x.inspect
280
- # }
281
- resolve(*goals) {|env|
282
- count += 1
283
- results << env[goals]
284
- # printout[env[goals]]
421
+ def query(&block)
422
+ goals = instance_eval(&block)
423
+ goals = [goals] unless goals.is_a?(Array)
424
+ results = []
425
+
426
+ resolve(*goals.map(&:to_goal)) {|env|
427
+ results << env.solution
285
428
  }
286
- # printout[goals] if count == 0
287
429
  return results
288
430
  end
289
-
290
-
431
+
432
+
291
433
  def is(*syms,&block)
292
434
  $is_cnt ||= 0
293
- is = Predicate.new "IS_#{$is_cnt += 1}"
435
+ is = @db.register("IS_#{$is_cnt += 1}", skip_listing: true)
294
436
  raise "At least one symbol needed" unless syms.size > 0
295
437
  is[*syms].calls do |env|
296
438
  value = block.call(*syms[1..-1].map{|x| env[x]})
@@ -300,46 +442,22 @@ module RubyProlog
300
442
  end
301
443
 
302
444
  def method_missing(meth, *args)
303
- # puts "NEW PRED #{meth} #{meth.class}"
304
- pred = Predicate.new(meth)
305
- # proc = Proc.new {pred}
306
-
307
-
308
- # We only want to define the method on this specific object instance to avoid polluting global namespaces.
309
-
310
- # You can't do this..
311
- # class << self
312
- # module_eval do
313
- # send(:define_method, m, proc)
314
- # end
315
- # end
316
-
317
- # Nor this..
318
- # define_method(meth) {pred}
319
-
320
- # Nor this..
321
- # self.send(:define_method, meth, proc)
322
-
323
- # And you don't want to pollute the global namespace like this...
324
- # Object.class_eval{ define_method(meth){pr} }
325
-
326
-
327
- # Sooooo... I know this doesn't really make intuitive sense,
328
- # but you need to get the eigenclass and then define
329
- # the method within that context in such a way that we
330
- # have access to local variables, like this...
331
- class << self; self; end.module_eval do
332
- define_method meth, Proc.new{pred}
333
- end
334
- # ...which is major fuglytown, but I don't know how to do it any other way.
445
+ pred = @db.register(meth)
446
+
447
+ # We only want to define the method on this specific object instance to avoid polluting global namespaces.
448
+ define_singleton_method(meth){ @db.by_name[meth] }
449
+
450
+ pred
451
+ end
335
452
 
336
- return pred
453
+ def to_prolog
454
+ @db.listing.map(&:to_prolog).join("\n\n")
337
455
  end
338
456
 
339
-
457
+
340
458
  def initialize
341
- # We do not need to predefine predicates like this because they will automatically be defined for us.
342
- # write = Predicate.new "write"
459
+ @db = Database.new
460
+ # These predicates are made available in all environments
343
461
  write[:X].calls{|env| print env[:X]; true}
344
462
  writenl[:X].calls{|env| puts env[:X]; true}
345
463
  nl[:X].calls{|e| puts; true}
@@ -358,8 +476,21 @@ module RubyProlog
358
476
  end
359
477
  end
360
478
  numeric[:X].calls{|env| Numeric === env[:X] }
479
+
480
+ not_[:X].calls do |env|
481
+ found_solution = false
482
+ resolve(env[:X], :CUT) { found_solution = true }
483
+ found_solution == false
484
+ end
485
+
486
+ # Enable here so the predicates above don't make it in to_prolog output
487
+ @db.enable_listing
361
488
  end
362
489
 
490
+ def initialize_copy(orig)
491
+ super
492
+ @db = @db.clone
493
+ end
363
494
  end
364
495
 
365
- end
496
+ end
@@ -1,5 +1,5 @@
1
1
  module RubyProlog
2
2
 
3
- VERSION = '1.0.1'
3
+ VERSION = '2.1.0'
4
4
 
5
5
  end
@@ -8,17 +8,18 @@ Gem::Specification.new do |spec|
8
8
  spec.version = RubyProlog::VERSION
9
9
  spec.authors = ["Preston Lee"]
10
10
  spec.email = ["preston.lee@prestonlee.com"]
11
- spec.description = "A Prolog-ish Ruby DSL."
11
+ spec.description = "A pure Ruby implementation of a useful subset of Prolog."
12
12
  spec.summary = "A Prolog-ish Ruby DSL."
13
13
  spec.homepage = "http://github.com/preston/ruby-prolog"
14
- spec.license = "Apache 2"
14
+ spec.license = "Apache-2.0"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "minitest"
21
+ spec.add_development_dependency "bundler", "~> 2"
22
+ spec.add_development_dependency "rake", "~> 13"
23
+ spec.add_development_dependency "minitest", "~> 5.14.0"
24
+ spec.add_development_dependency "minitest-focus", "~> 1.1.2"
24
25
  end
@@ -3,51 +3,114 @@ require_relative '../../test_helper'
3
3
 
4
4
 
5
5
 
6
- describe RubyProlog do
7
-
6
+ describe RubyProlog do
7
+
8
8
  it 'should not pollute the global namespace with predicates.' do
9
-
9
+
10
10
  # We'll create numerous instances of the engine and assert they do not interfere with each other.
11
- one = RubyProlog::Core.new
12
- one.instance_eval do
13
- query(male[:X]).length.must_equal 0
14
- end
15
-
16
- two = RubyProlog::Core.new
17
- two.instance_eval do
18
- male[:preston].fact
19
- query(male[:X]).length.must_equal 1
20
- end
21
-
22
- three = RubyProlog::Core.new
23
- three.instance_eval do
24
- query(male[:X]).length.must_equal 0
25
- end
26
-
27
- one.instance_eval do
28
- query(male[:X]).length.must_equal 0
29
- end
11
+ one = RubyProlog::Core.new
12
+ _( one.query{ male[:X] }.length ).must_equal 0
13
+
14
+ two = RubyProlog::Core.new
15
+ two.instance_eval do
16
+ male[:preston].fact
17
+ end
18
+ _( two.query{ male[:X] }.length ).must_equal 1
30
19
 
20
+ three = RubyProlog::Core.new
21
+ _( three.query{ male[:X] }.length ).must_equal 0
22
+
23
+ _( one.query{ male[:X] }.length ).must_equal 0
31
24
  end
32
-
33
25
 
34
26
 
27
+ it 'returns hashes of solutions' do
28
+ one = RubyProlog.new do
29
+ foo['a', 'b'].fact
30
+ foo['a', 'b'].fact
31
+ foo['a', 'c'].fact
32
+ foo['d', 'e'].fact
33
+ foo['d', 'c'].fact
34
+ end
35
+ _( one.query {_= foo['a', :X] } ).must_equal [{ X: 'b' }, { X: 'b' }, { X: 'c' }]
36
+ _( one.query {_= foo['a', :X], foo['d', :X] } ).must_equal [{ X: 'c' }]
37
+ _(one.to_prolog.class).must_equal String
38
+ end
39
+
40
+ it 'works with numbers' do
41
+ one = RubyProlog.new do
42
+ foo[10, 20].fact
43
+ foo[10, 30].fact
44
+ end
45
+ _( one.query {_= foo[10, :X] } ).must_equal [{ X: 20 }, { X: 30 }]
46
+
47
+ _(one.to_prolog.class).must_equal String
48
+ end
49
+
50
+ it 'considers all predicates dynamic' do
51
+ one = RubyProlog::Core.new
52
+ one.instance_eval do
53
+ foo[10] << [bar[20]]
54
+ end
55
+ _( one.query {_= foo[:X] } ).must_equal []
56
+ end
57
+
58
+ it 'supports underscore' do
59
+ one = RubyProlog::Core.new
60
+ one.instance_eval do
61
+ foo[10, 200].fact
62
+ foo[10, 300].fact
63
+ foo[20, 400].fact
64
+
65
+ bar[50, :_].fact
66
+ end
67
+ _( one.query { foo[:X, :_] } ).must_equal [{X: 10}, {X: 10}, {X: 20}]
68
+ _( one.query { bar[50, 99] } ).must_equal [{}]
69
+ one.to_prolog
70
+ end
71
+
72
+ it 'supports clone' do
73
+ one = RubyProlog::Core.new
74
+ one.instance_eval do
75
+ foo[10].fact
76
+ end
77
+ _( one.query {_= foo[:X] } ).must_equal [{X: 10}]
78
+
79
+ two = one.clone
80
+ _( one.query {_= foo[:X] } ).must_equal [{X: 10}]
81
+ _( two.query {_= foo[:X] } ).must_equal [{X: 10}]
82
+
83
+ one.instance_eval{ foo[20].fact }
84
+
85
+ _( one.query {_= foo[:X] } ).must_equal [{X: 10}, {X: 20}]
86
+ _( two.query {_= foo[:X] } ).must_equal [{X: 10}]
87
+
88
+ two.instance_eval{ foo[30].fact }
89
+ _( one.query {_= foo[:X] } ).must_equal [{X: 10}, {X: 20}]
90
+ _( two.query {_= foo[:X] } ).must_equal [{X: 10}, {X: 30}]
91
+ end
92
+
35
93
  it 'should be able to query simple family trees.' do
36
94
 
37
- c = RubyProlog::Core.new
38
- c.instance_eval do
95
+ c = RubyProlog.new do
39
96
  # Basic family tree relationships..
40
- sibling[:X,:Y] <<= [ parent[:Z,:X], parent[:Z,:Y], noteq[:X,:Y] ]
41
- mother[:X,:Y] <<= [parent[:X, :Y], female[:X]]
42
- father[:X,:Y] <<= [parent[:X, :Y], male[:X]]
43
- grandparent[:G,:C] <<= [ parent[:G,:P], parent[:P,:C]]
44
- ancestor[:A, :C] <<= [parent[:A, :X], parent[:X, :B]]
45
- mothers[:M, :C] <<= mother[:M, :C]
46
- mothers[:M, :C] <<= [mother[:M, :X], mothers[:X, :C]]
47
- fathers[:F, :C] <<= father[:F, :C]
48
- fathers[:F, :C] <<= [father[:F, :X], fathers[:X, :C]]
49
- widower[:W] <<= [married[:W, :X], deceased[:X], nl[deceased[:W]]]
50
- widower[:W] <<= [married[:X, :W], deceased[:X], nl[deceased[:W]]]
97
+ sibling[:X,:Y] << [ parent[:Z,:X], parent[:Z,:Y], noteq[:X,:Y] ]
98
+ mother[:X,:Y] << [parent[:X, :Y], female[:X]]
99
+ father[:X,:Y] << [parent[:X, :Y], male[:X]]
100
+
101
+ grandparent[:G,:C] << [ parent[:G,:P], parent[:P,:C]]
102
+
103
+ ancestor[:A, :C] << [parent[:A, :C]]
104
+ ancestor[:A, :C] << [parent[:A, :X], parent[:X, :C]]
105
+
106
+ mothers[:M, :C] << mother[:M, :C]
107
+ mothers[:M, :C] << [mother[:M, :X], mothers[:X, :C]]
108
+
109
+ fathers[:F, :C] << father[:F, :C]
110
+ fathers[:F, :C] << [father[:F, :X], fathers[:X, :C]]
111
+
112
+ widower[:W] << [married[:W, :X], deceased[:X], nl[deceased[:W]]]
113
+ widower[:W] << [married[:X, :W], deceased[:X], nl[deceased[:W]]]
51
114
 
52
115
  # Basic parents relationships as could be stored in a typical relational database.
53
116
  parent['Ms. Old', 'Marge'].fact
@@ -108,108 +171,137 @@ describe RubyProlog do
108
171
  interest['Karen', 'Walks'].fact
109
172
  interest['Ron', 'Walks'].fact
110
173
  interest['Marcia', 'Walks'].fact
111
-
112
- # Runs some queries..
113
-
114
- # p "Who are Silas's parents?"
115
- # Silas should have two parents: Matt and Julie.
116
- r = query(parent[:P, 'Silas'])
117
- r.length.must_equal 2
118
- r[0][0].args[0].must_equal 'Matt'
119
- r[1][0].args[0].must_equal 'Julie'
120
-
121
- # p "Who is married?"
122
- # We defined 5 married facts.
123
- query(married[:A, :B]).length.must_equal 5
124
-
125
- # p 'Are Karen and Julie siblings?'
126
- # Yes, through two parents.
127
- query(sibling['Karen', 'Julie']).length.must_equal 2
128
-
129
-
130
- # p "Who likes to play games?"
131
- # Four people.
132
- query(interest[:X, 'Games']).length.must_equal 4
133
-
134
-
135
- # p "Who likes to play checkers?"
136
- # Nobody.
137
- query(interest[:X, 'Checkers']).length.must_equal 0
138
-
139
- # p "Who are Karen's ancestors?"
140
- # query(ancestor[:A, 'Karen'])
141
-
142
- # p "What grandparents are also widowers?"
143
- # Marge, twice, because of two grandchildren.
144
- query(widower[:X], grandparent[:X, :G]).length.must_equal 2
145
174
  end
146
175
 
176
+ # Runs some queries..
177
+
178
+ # p "Who are Silas's parents?"
179
+ # Silas should have two parents: Matt and Julie.
180
+ _( c.query{ parent[:P, 'Silas'] } ).must_equal [{P: 'Matt'}, {P: 'Julie'}]
181
+
182
+ # p "Who is married?"
183
+ # We defined 5 married facts.
184
+ _( c.query{ married[:A, :B] }.length ).must_equal 5
185
+
186
+ # p 'Are Karen and Julie siblings?'
187
+ # Yes, through two parents.
188
+ _( c.query{ sibling['Karen', 'Julie'] }.length ).must_equal 2
189
+
190
+
191
+ # p "Who likes to play games?"
192
+ # Four people.
193
+ _( c.query{ interest[:X, 'Games'] }.length ).must_equal 4
194
+
195
+
196
+ # p "Who likes to play checkers?"
197
+ # Nobody.
198
+ _( c.query{ interest[:X, 'Checkers'] }.length ).must_equal 0
199
+
200
+ # p "Who are Karen's ancestors?"
201
+ _( c.query{ ancestor[:A, 'Karen'] } ).must_equal [
202
+ {A: 'Marcia'},
203
+ {A: 'Ron'},
204
+ {A: 'Carol'},
205
+ {A: 'Kent'},
206
+ {A: 'Marge'},
207
+ {A: 'Pappy'},
208
+ ]
209
+
210
+ # p "What grandparents are also widowers?"
211
+ # Marge, twice, because of two grandchildren.
212
+ _( c.query{_= widower[:X], grandparent[:X, :G] }.length ).must_equal 2
213
+
214
+ _(c.to_prolog.class).must_equal String
147
215
  end
148
216
 
149
217
 
150
218
  it 'should be able to query simple family trees.' do
151
219
 
152
- c = RubyProlog::Core.new
153
- c.instance_eval do
220
+ c = RubyProlog.new do
154
221
 
155
222
  vendor['dell'].fact
156
223
  vendor['apple'].fact
157
-
224
+
158
225
  model['ultrasharp'].fact
159
226
  model['xps'].fact
160
227
  model['macbook'].fact
161
228
  model['iphone'].fact
162
-
229
+
163
230
  manufactures['dell', 'ultrasharp'].fact
164
231
  manufactures['dell', 'xps'].fact
165
232
  manufactures['apple', 'macbook'].fact
166
233
  manufactures['apple', 'iphone'].fact
167
-
234
+
168
235
  is_a['xps', 'laptop'].fact
169
236
  is_a['macbook', 'laptop'].fact
170
237
  is_a['ultrasharp', 'monitor'].fact
171
238
  is_a['iphone', 'phone'].fact
172
-
173
- kind['laptop']
174
- kind['monitor']
175
- kind['phone']
176
-
177
- model[:M] <<= [manfactures[:V, :M]]
178
-
179
- vendor_of[:V, :K] <<= [vendor[:V], manufactures[:V, :M], is_a[:M, :K]]
180
- # not_vendor_of[:V, :K] <<= [vendor[:V], nl[vendor_of[:V, :K]]]
181
-
182
- query(is_a[:K, 'laptop']).length == 2
183
- query(vendor_of[:V, 'phone']) == 1
184
- # pp query(not_vendor_of[:V, 'phone'])
239
+
240
+ kind['laptop'].fact
241
+ kind['monitor'].fact
242
+ kind['phone'].fact
243
+
244
+ model[:M] << [manfactures[:V, :M]]
245
+
246
+ vendor_of[:V, :K] << [vendor[:V], manufactures[:V, :M], is_a[:M, :K]]
247
+ not_vendor_of[:V, :K] << [vendor[:V], not_[vendor_of[:V, :K]]]
185
248
  end
186
-
249
+
250
+ _( c.query{ is_a[:K, 'laptop'] }.length ).must_equal 2
251
+ _( c.query{ vendor_of[:V, 'phone'] } ).must_equal [{V: 'apple'}]
252
+ _( c.query{ not_vendor_of[:V, 'phone'] } ).must_equal [{V: 'dell'}]
253
+ _(c.to_prolog.class).must_equal String
187
254
  end
188
-
189
-
255
+
256
+
190
257
  it 'should solve the Towers of Hanoi problem.' do
191
- c = RubyProlog::Core.new
192
- c.instance_eval do
258
+ c = RubyProlog.new do
193
259
 
194
- move[0,:X,:Y,:Z] <<= :CUT # There are no more moves left
195
- move[:N,:A,:B,:C] <<= [
260
+ move[0,:X,:Y,:Z] << :CUT # There are no more moves left
261
+ move[:N,:A,:B,:C] << [
196
262
  is(:M,:N){|n| n - 1}, # reads as "M IS N - 1"
197
263
  move[:M,:A,:C,:B],
198
264
  # write_info[:A,:B],
199
265
  move[:M,:C,:B,:A]
200
266
  ]
201
- write_info[:X,:Y] <<= [
267
+ write_info[:X,:Y] << [
202
268
  # write["move a disc from the "],
203
269
  # write[:X], write[" pole to the "],
204
270
  # write[:Y], writenl[" pole "]
205
271
  ]
206
272
 
207
- hanoi[:N] <<= move[:N,"left","right","center"]
208
- query(hanoi[5]).length.must_equal 1
273
+ hanoi[:N] << move[:N,"left","right","center"]
274
+ end
275
+
276
+ _( c.query{ hanoi[5] } ).must_equal [{}]
277
+
278
+ _(c.to_prolog.class).must_equal String
279
+ end
280
+
281
+ it 'works on the other examples in the readme' do
282
+ db = RubyProlog.new do
283
+ implication['a', 'b'].fact
284
+ implication['b', 'c'].fact
285
+ implication['c', 'd'].fact
286
+ implication['c', 'x'].fact
287
+
288
+ implies[:A, :B] << implication[:A, :B]
289
+ implies[:A, :B] << [
290
+ implication[:A, :Something],
291
+ implies[:Something, :B]
292
+ ]
293
+ end
294
+
295
+ _( db.query{ implication['c', :X] } ).must_equal [{ X: 'd' }, { X: 'x' }]
296
+ _( db.query{ implication[:X, :_] } ).must_equal [{ X: 'a' }, { X: 'b' }, { X: 'c' }, { X: 'c' }]
297
+ _( db.query{_= implies['a', :X] } ).must_equal [{ X: 'b' }, { X: 'c' }, { X: 'd' }, { X: 'x' }]
209
298
 
210
- # do_stuff[:STUFF].calls{|env| print env[:STUFF]; true}
299
+ _( db.query{[ implication['b', :S], implies[:S, :B] ]} ).must_equal [{:S=>"c", :B=>"d"}, {:S=>"c", :B=>"x"}]
300
+ _( db.query{_= implication['b', :S], implies[:S, :B] } ).must_equal [{:S=>"c", :B=>"d"}, {:S=>"c", :B=>"x"}]
211
301
 
212
- end
213
-
302
+ # For good measure
303
+ _( db.query{_= implies['a', 'b'] } ).must_equal [{}]
304
+ _( db.query{_= implies['a', 'd'] } ).must_equal [{}]
305
+ _( db.query{_= implies['a', 'idontexist'] } ).must_equal []
214
306
  end
215
307
  end
@@ -1,3 +1,4 @@
1
1
  require 'minitest/autorun'
2
2
  require 'minitest/pride'
3
+ require 'minitest/focus'
3
4
  require File.expand_path('../../lib/ruby-prolog.rb', __FILE__)
metadata CHANGED
@@ -1,58 +1,72 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-prolog
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Preston Lee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-07-31 00:00:00.000000000 Z
11
+ date: 2020-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '13'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '13'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 5.14.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
55
- description: A Prolog-ish Ruby DSL.
54
+ version: 5.14.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-focus
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.2
69
+ description: A pure Ruby implementation of a useful subset of Prolog.
56
70
  email:
57
71
  - preston.lee@prestonlee.com
58
72
  executables:
@@ -61,7 +75,7 @@ executables:
61
75
  extensions: []
62
76
  extra_rdoc_files: []
63
77
  files:
64
- - .gitignore
78
+ - ".gitignore"
65
79
  - Gemfile
66
80
  - NOTICE
67
81
  - README.md
@@ -76,7 +90,7 @@ files:
76
90
  - test/test_helper.rb
77
91
  homepage: http://github.com/preston/ruby-prolog
78
92
  licenses:
79
- - Apache 2
93
+ - Apache-2.0
80
94
  metadata: {}
81
95
  post_install_message:
82
96
  rdoc_options: []
@@ -84,17 +98,16 @@ require_paths:
84
98
  - lib
85
99
  required_ruby_version: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - '>='
101
+ - - ">="
88
102
  - !ruby/object:Gem::Version
89
103
  version: '0'
90
104
  required_rubygems_version: !ruby/object:Gem::Requirement
91
105
  requirements:
92
- - - '>='
106
+ - - ">="
93
107
  - !ruby/object:Gem::Version
94
108
  version: '0'
95
109
  requirements: []
96
- rubyforge_project:
97
- rubygems_version: 2.0.3
110
+ rubygems_version: 3.1.2
98
111
  signing_key:
99
112
  specification_version: 4
100
113
  summary: A Prolog-ish Ruby DSL.