porolog 0.0.8 → 1.0.0

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.
@@ -20,7 +20,7 @@ module Porolog
20
20
  # The actual arguments.
21
21
  class Arguments
22
22
 
23
- attr_reader :predicate, :arguments
23
+ attr_reader :predicate, :arguments, :block
24
24
 
25
25
  # Unregisters all Arguments
26
26
  # @return [true]
@@ -34,9 +34,10 @@ module Porolog
34
34
  # Creates a new Arguments for a Predicate
35
35
  # @param predicate [Porolog::Predicate] the Predicate for which these are the arguments
36
36
  # @param arguments [Array<Object>] the actual arguments
37
- def initialize(predicate, arguments)
37
+ def initialize(predicate, arguments, &block)
38
38
  @predicate = predicate
39
39
  @arguments = arguments
40
+ @block = block
40
41
  @@arguments << self
41
42
  end
42
43
 
@@ -54,7 +55,8 @@ module Porolog
54
55
 
55
56
  # @return [String] pretty representation
56
57
  def inspect
57
- "#{@predicate && @predicate.name}(#{@arguments && @arguments.map(&:inspect).join(',')})"
58
+ block_inspect = block.nil? ? '' : "{#{block.inspect}}"
59
+ "#{@predicate&.name}(#{@arguments&.map(&:inspect).join(',')})#{block_inspect}"
58
60
  end
59
61
 
60
62
  # Creates a fact rule that states that these arguments satisfy the Predicate.
@@ -103,9 +105,7 @@ module Porolog
103
105
 
104
106
  # @return [Array<Symbol>] the variables contained in the arguments
105
107
  def variables
106
- @arguments.map{|argument|
107
- argument.variables
108
- }.flatten.uniq
108
+ @arguments.map(&:variables).flatten.uniq
109
109
  end
110
110
 
111
111
  # Creates a Goal for solving this Arguments for the Predicate
@@ -120,7 +120,7 @@ module Porolog
120
120
  # @return [Array<Hash{Symbol => Object}>] the solutions found (memoized)
121
121
  def solutions(max_solutions = nil)
122
122
  @solutions ||= solve(max_solutions)
123
- @solutions
123
+ max_solutions && @solutions[0...max_solutions] || @solutions
124
124
  end
125
125
 
126
126
  # Solves the Arguments
@@ -132,6 +132,7 @@ module Porolog
132
132
 
133
133
  # Extracts solution values.
134
134
  # @param variables [Symbol, Array<Symbol>] variable or variables
135
+ # @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
135
136
  # @return [Array<Object>] all the values for the variables given
136
137
  # @example
137
138
  # predicate :treasure_at
@@ -140,9 +141,9 @@ module Porolog
140
141
  # xs = arguments.solve_for(:X)
141
142
  # ys = arguments.solve_for(:Y)
142
143
  # coords = xs.zip(ys)
143
- def solve_for(*variables)
144
+ def solve_for(*variables, max_solutions: nil)
144
145
  variables = [*variables]
145
- solutions.map{|solution|
146
+ solutions(max_solutions).map{|solution|
146
147
  values = variables.map{|variable| solution[variable] }
147
148
  if values.size == 1
148
149
  values.first
@@ -154,14 +155,14 @@ module Porolog
154
155
 
155
156
  # @return [Boolean] whether any solutions were found
156
157
  def valid?
157
- !solutions.empty?
158
+ !solutions(1).empty?
158
159
  end
159
160
 
160
161
  # Duplicates the Arguments in the context of the given goal
161
162
  # @param goal [Porolog::Goal] the destination goal
162
163
  # @return [Porolog::Arguments] the duplicated Arguments
163
164
  def dup(goal)
164
- self.class.new @predicate, goal.variablise(arguments)
165
+ self.class.new @predicate, goal.variablise(arguments), &@block
165
166
  end
166
167
 
167
168
  # @param other [Porolog::Arguments] arguments for comparison
@@ -14,7 +14,7 @@ class Object
14
14
  # A convenience method for testing/debugging.
15
15
  # @return [String] the equivalent of inspect.
16
16
  def myid
17
- self.inspect
17
+ inspect
18
18
  end
19
19
 
20
20
  # @return [Array] embedded variables (for an Object, should be none).
@@ -58,7 +58,7 @@ class Symbol
58
58
  # A convenience method for testing/debugging.
59
59
  # @return [String] the equivalent of inspect.
60
60
  def myid
61
- self.inspect
61
+ inspect
62
62
  end
63
63
 
64
64
  # @return [Array] embedded variables (for a Symbol, should be itself).
@@ -89,18 +89,20 @@ class Array
89
89
  end
90
90
 
91
91
  # @return [Array] the values of its elements.
92
- def value(*args)
93
- map{|element|
92
+ def value(visited = [])
93
+ return self if visited.include?(self)
94
+ visited = visited + [self]
95
+ flat_map{|element|
94
96
  if element.is_a?(Tail)
95
- tail = element.value(*args)
97
+ tail = element.value(visited)
96
98
  if tail.is_a?(Array)
97
99
  tail
98
100
  elsif tail.is_a?(Variable) || tail.is_a?(Value)
99
- tail = tail.value(*args)
101
+ tail = tail.value(visited)
100
102
  if tail.is_a?(Array)
101
103
  tail
102
104
  elsif tail.is_a?(Variable) || tail.is_a?(Value)
103
- tail = tail.goal.variablise(tail.value(*args))
105
+ tail = tail.goal.variablise(tail.value(visited))
104
106
  if tail.is_a?(Array)
105
107
  tail
106
108
  else
@@ -113,9 +115,25 @@ class Array
113
115
  [element]
114
116
  end
115
117
  else
116
- [element.value(*args)]
118
+ [element.value(visited)]
117
119
  end
118
- }.flatten(1)
120
+ }
121
+ end
122
+
123
+ # Removes Porolog processing objects.
124
+ # @return [Array] the values of its elements with variables replaced by nil and Tails replaced by UNKNOWN_TAIL.
125
+ def clean
126
+ value.map{|element|
127
+ if element.is_a?(Array)
128
+ element.clean
129
+ elsif element.is_a?(Tail)
130
+ UNKNOWN_TAIL
131
+ elsif element.is_a?(Variable)
132
+ nil
133
+ else
134
+ element.value
135
+ end
136
+ }
119
137
  end
120
138
 
121
139
  # @return [Symbol] the type of the object (for an Array, should be :array)
@@ -15,5 +15,8 @@ module Porolog
15
15
 
16
16
  # Error indicating that a Goal is needed but could not be found.
17
17
  class NoGoalError < PorologError ; end
18
+
19
+ # Error indicating that a Variable is needed but was not provided.
20
+ class NonVariableError < PorologError ; end
18
21
 
19
22
  end
@@ -39,10 +39,16 @@ module Porolog
39
39
  @@goals ||= []
40
40
  goals = @@goals
41
41
  @@goals = []
42
+ @@goal_count = 0
42
43
  goals.map(&:deleted?)
43
44
  true
44
45
  end
45
46
 
47
+ # @return [Integer] the number of goals created
48
+ def self.goal_count
49
+ @@goal_count
50
+ end
51
+
46
52
  reset
47
53
 
48
54
  attr_accessor :calling_goal, :arguments, :index, :result, :description, :log
@@ -52,6 +58,7 @@ module Porolog
52
58
  # @param calling_goal [Porolog::Goal] the parent Goal if this Goal is a subgoal.
53
59
  def initialize(arguments, calling_goal = nil)
54
60
  @@goals << self
61
+ @@goal_count += 1
55
62
 
56
63
  @arguments = arguments
57
64
  @terminate = false
@@ -124,12 +131,12 @@ module Porolog
124
131
  def check_deleted
125
132
  return false if @@goals.include?(self)
126
133
 
127
- @variables.delete_if do |name,variable|
134
+ @variables.delete_if do |_name,variable|
128
135
  variable.remove
129
136
  true
130
137
  end
131
138
 
132
- @values.delete_if do |name,value|
139
+ @values.delete_if do |_name,value|
133
140
  value.remove
134
141
  true
135
142
  end
@@ -170,7 +177,7 @@ module Porolog
170
177
  when NilClass
171
178
  nil
172
179
  else
173
- if object == UNKNOWN_TAIL || object == UNKNOWN_ARRAY
180
+ if [UNKNOWN_TAIL, UNKNOWN_ARRAY].include?(object)
174
181
  object
175
182
  else
176
183
  value(object)
@@ -178,17 +185,16 @@ module Porolog
178
185
  end
179
186
  end
180
187
 
181
- alias_method :[], :variablise
188
+ alias [] variablise
182
189
 
183
190
  # @return [Hash{Symbol => Object}] the Variables and their current values of this Goal
184
191
  def variables
185
192
  @variables.keys.each_with_object({}){|variable,variable_list|
186
- value = value_of(variable)
187
- value = value.value if value.is_a?(Value)
193
+ value = value_of(variable).value.value
188
194
  if value.is_a?(Variable)
189
195
  variable_list[variable] = nil
190
- elsif value.is_a?(Value) || value.is_a?(Array)
191
- variable_list[variable] = value.value
196
+ elsif value.is_a?(Array)
197
+ variable_list[variable] = value.clean
192
198
  else
193
199
  variable_list[variable] = value
194
200
  end
@@ -198,15 +204,13 @@ module Porolog
198
204
  # A convenience method for testing/debugging.
199
205
  # @return [String] a tree representation of all the instantiations of this goal's variables.
200
206
  def inspect_variables
201
- @variables.map{|name,variable|
202
- variable.inspect_with_instantiations
203
- }.join("\n")
207
+ @variables.values.map(&:inspect_with_instantiations).join("\n")
204
208
  end
205
209
 
206
210
  # A convenience method for testing/debugging.
207
211
  # @return [Array<Object>] the values instantiated for this goal.
208
212
  def values
209
- @values.map{|name,value| value.value }
213
+ @values.values.map(&:value)
210
214
  end
211
215
 
212
216
  # Finds or tries to create a variable in the goal (as much as possible) otherwise passes the parameter back.
@@ -267,7 +271,9 @@ module Porolog
267
271
 
268
272
  predicate = @arguments.predicate
269
273
 
270
- predicate && predicate.satisfy(self) do |goal|
274
+ predicate&.satisfy(self) do |goal|
275
+ # TODO: Refactor to overrideable method (or another solution, say a lambda)
276
+
271
277
  @solutions << variables
272
278
  @log << "SOLUTION: #{variables}"
273
279
  @log << goal.ancestry
@@ -288,9 +294,12 @@ module Porolog
288
294
 
289
295
  predicate = @arguments.predicate
290
296
 
291
- predicate && predicate.satisfy(self) do |subgoal|
292
- block.call(subgoal)
297
+ satisfied = false
298
+ predicate&.satisfy(self) do |subgoal|
299
+ subgoal_satisfied = block.call(subgoal)
300
+ satisfied ||= subgoal_satisfied
293
301
  end
302
+ satisfied
294
303
  end
295
304
 
296
305
  # Instantiates a Variable to another Variable or Value, for this Goal.
@@ -310,6 +319,39 @@ module Porolog
310
319
  variable.instantiate(other)
311
320
  end
312
321
 
322
+ # Inherits variables and their instantiations from another goal.
323
+ # @param other_goal [Porolog::Goal,nil] the Goal to inherit variables from.
324
+ # @return [Boolean] whether the variables could be inherited (unified).
325
+ def inherit_variables(other_goal = @calling_goal)
326
+ return true unless other_goal
327
+
328
+ unified = true
329
+ unifications = []
330
+ variables = (
331
+ other_goal.arguments.variables +
332
+ other_goal.variables.keys +
333
+ self.arguments.variables
334
+ ).map(&:to_sym).uniq
335
+
336
+ variables.each do |variable|
337
+ name = variable
338
+
339
+ unification = unify(name, name, other_goal, self)
340
+ unified &&= !!unification
341
+ if unified
342
+ unifications += unification
343
+ else
344
+ #:nocov:
345
+ self.log << "Couldn't unify: #{name.inspect} WITH #{other_goal.myid} AND #{self.myid}"
346
+ break
347
+ #:nocov:
348
+ end
349
+ end
350
+ unified &&= instantiate_unifications(unifications) if unified
351
+
352
+ unified
353
+ end
354
+
313
355
  end
314
356
 
315
357
  end
@@ -121,7 +121,7 @@ module Porolog
121
121
  return nil unless variables.include?(variable)
122
122
 
123
123
  other_variable = (variables - [variable]).first
124
- other_variable && other_variable.goal
124
+ other_variable&.goal
125
125
  end
126
126
 
127
127
  # @return [Array<Porolog::Goal>] the Goals of the Variables of the Instantiation.
@@ -139,10 +139,10 @@ module Porolog
139
139
  def values(visited = [])
140
140
  return [] if visited.include?(self)
141
141
 
142
- vv1 = values_for(@variable1, visited)
143
- vv2 = values_for(@variable2, visited)
142
+ values_for_variable1 = values_for(@variable1, visited)
143
+ values_for_variable2 = values_for(@variable2, visited)
144
144
 
145
- (vv1 + vv2).uniq
145
+ (values_for_variable1 + values_for_variable2).uniq
146
146
  end
147
147
 
148
148
  # @param value [Object] the provided value.
@@ -174,7 +174,21 @@ module Porolog
174
174
 
175
175
  when Symbol
176
176
  value_value = value.value(visited)
177
- if value_value.respond_to?(index)
177
+ if index == :flathead
178
+ # value_value = [..., 4, 5, 6]
179
+ if value_value.first == UNKNOWN_TAIL
180
+ UNKNOWN_ARRAY
181
+ else
182
+ nil
183
+ end
184
+ elsif index == :flattail
185
+ # value_value = [1, 2, 3, ...]
186
+ if value_value.first == UNKNOWN_TAIL
187
+ nil
188
+ elsif value_value.last == UNKNOWN_TAIL
189
+ UNKNOWN_ARRAY
190
+ end
191
+ elsif value_value.respond_to?(index)
178
192
  value_value.send(index)
179
193
  else
180
194
  value
@@ -204,6 +218,22 @@ module Porolog
204
218
  visited = visited + [self]
205
219
 
206
220
  if variable == @variable1
221
+ if @index1 == :flathead
222
+ flathead = value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1).value.value
223
+ if flathead
224
+ return [[*flathead]]
225
+ else
226
+ return []
227
+ end
228
+ end
229
+ if @index1 == :flattail
230
+ flattail = value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1).value.value
231
+ if flattail
232
+ return [[UNKNOWN_TAIL, *flattail]]
233
+ else
234
+ return []
235
+ end
236
+ end
207
237
  if @index1
208
238
  [value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1)]
209
239
  else
@@ -214,6 +244,22 @@ module Porolog
214
244
  end
215
245
  end
216
246
  elsif variable == @variable2
247
+ if @index2 == :flathead
248
+ flathead = value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2).value.value
249
+ if flathead
250
+ return [[*flathead]]
251
+ else
252
+ return []
253
+ end
254
+ end
255
+ if @index2 == :flattail
256
+ flattail = value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2).value.value
257
+ if flattail
258
+ return [[UNKNOWN_TAIL, *flattail]]
259
+ else
260
+ return []
261
+ end
262
+ end
217
263
  if @index2
218
264
  [value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2)]
219
265
  else
@@ -241,10 +287,14 @@ module Porolog
241
287
 
242
288
  when Symbol
243
289
  case index
290
+ when :flathead
291
+ [*value, UNKNOWN_TAIL]
244
292
  when :head
245
293
  [value, UNKNOWN_TAIL]
246
294
  when :tail
247
295
  [nil, *value]
296
+ when :flattail
297
+ value
248
298
  else
249
299
  raise UnhandledIndexError, "Unhandled index: #{index.inspect} for #{value.inspect}"
250
300
  end
@@ -253,10 +303,6 @@ module Porolog
253
303
  if index.empty?
254
304
  [nil, *value]
255
305
  else
256
- #result = []
257
- #result[0..index.first] = value
258
- #result
259
- #[*([*value][0..index.first]), UNKNOWN_TAIL]
260
306
  [value, UNKNOWN_TAIL]
261
307
  end
262
308
 
@@ -28,6 +28,10 @@ module Porolog
28
28
 
29
29
  attr_reader :name, :rules
30
30
 
31
+ # A unique value used to verify instantiations.
32
+ UNIQUE_VALUE = Object.new.freeze
33
+ private_constant :UNIQUE_VALUE
34
+
31
35
  # Returns the current scope, or sets the current scope if a paramter is provided
32
36
  # (creating the new scope if necessary).
33
37
  # @param scope_name [Object] the name (or otherwise object) used to register a scope.
@@ -67,9 +71,10 @@ module Porolog
67
71
  # Initializes a Porolog::Predicate and registers it by its name.
68
72
  # @param name [#to_sym] the input object to read from
69
73
  # @param scope_name the name of the scope in which to register the Predicate; if omitted, defaults to the name of the current scope
70
- def initialize(name, scope_name = Predicate.scope.name)
71
- @name = name.to_sym
72
- @rules = []
74
+ def initialize(name, scope_name = Predicate.scope.name, builtin: false)
75
+ @name = name.to_sym
76
+ @rules = []
77
+ @builtin = builtin
73
78
 
74
79
  raise NameError, "Cannot name a predicate 'predicate'" if @name == :predicate
75
80
 
@@ -80,8 +85,7 @@ module Porolog
80
85
  # Creates a new Predicate, or returns an existing Predicate if one already exists with the given name in the current scope.
81
86
  # @return [Porolog::Predicate] a new or existing Predicate.
82
87
  def self.new(*args)
83
- name, _ = *args
84
- scope[name.to_sym] || super
88
+ scope[args.first.to_sym] || super
85
89
  end
86
90
 
87
91
  # Create Arguments for the Predicate.
@@ -90,14 +94,14 @@ module Porolog
90
94
  # for
91
95
  # p.arguments(x,y,z)
92
96
  # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
93
- def call(*args)
94
- Arguments.new(self,args)
97
+ def call(*args, &block)
98
+ Arguments.new(self, args, &block)
95
99
  end
96
100
 
97
101
  # Create Arguments for the Predicate.
98
102
  # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
99
- def arguments(*args)
100
- Arguments.new(self,args)
103
+ def arguments(*args, &block)
104
+ Arguments.new(self, args, &block)
101
105
  end
102
106
 
103
107
  # Add a Rule to the Predicate.
@@ -120,31 +124,53 @@ module Porolog
120
124
  end
121
125
  end
122
126
 
123
- # Return a builtin Predicate based on its key.
124
- # @param key [Symbol] the name (or otherwise object) used to register a scope.
125
- # @return [Porolog::Predicate] a Predicate with the next id based on the key.
126
- def self.builtin(key)
127
- @builtin_predicate_ids[key] ||= 0
128
- @builtin_predicate_ids[key] += 1
129
-
130
- self.new("_#{key}_#{@builtin_predicate_ids[key]}")
131
- end
132
-
133
127
  # Satisfy the Predicate within the supplied Goal.
134
128
  # Satisfy of each rule of the Predicate is called with the Goal and success block.
135
129
  # @param goal [Porolog::Goal] the Goal within which to satisfy the Predicate.
136
130
  # @param block [Proc] the block to be called each time a Rule of the Predicate is satisfied.
137
131
  # @return [Boolean] whether any Rule was satisfied.
138
132
  def satisfy(goal, &block)
139
- satisfied = false
140
- @rules.each do |rule|
141
- rule.satisfy(goal) do |subgoal|
142
- satisfied = true
143
- block.call(subgoal)
133
+ if builtin?
134
+ satisfy_builtin(goal, &block)
135
+ else
136
+ satisfied = false
137
+ @rules.each do |rule|
138
+ rule.satisfy(goal) do |subgoal|
139
+ satisfied = true
140
+ block.call(subgoal)
141
+ end
142
+ break if goal.terminated?
144
143
  end
145
- break if goal.terminated?
144
+ satisfied
146
145
  end
147
- satisfied
146
+ end
147
+
148
+ # @return [Boolean] whether the Predicate is a builtin predicate.
149
+ def builtin?
150
+ @builtin
151
+ end
152
+
153
+ # Satisfy the builtin Predicate within the supplied Goal.
154
+ # Call the builtin Predicate method with the Goal and success block.
155
+ # The arguments and block are extracted from the Goal's Arguments.
156
+ # @param goal [Porolog::Goal] the Goal within which to satisfy the Predicate.
157
+ # @param block [Proc] the block to be called each time a Rule of the Predicate is satisfied.
158
+ # @return [Boolean] whether any Rule was satisfied.
159
+ def satisfy_builtin(goal, &block)
160
+ predicate = goal.arguments.predicate.name
161
+ arguments = goal.arguments.arguments
162
+ arg_block = goal.arguments.block
163
+
164
+ Predicate.call_builtin(predicate, goal, block, *arguments, &arg_block)
165
+ end
166
+
167
+ # Call a builtin predicate
168
+ # @param predicate [Symbol] the name of the predicate to call.
169
+ # @param args [Array<Object>] arguments for the predicate call.
170
+ # @param arg_block [Proc] the block provided in the Arguments of the Predicate.
171
+ # @return [Boolean] whether the predicate was satisfied.
172
+ def self.call_builtin(predicate, *args, &arg_block)
173
+ Porolog::Predicate::Builtin.instance_method(predicate).bind(self).call(*args, &arg_block)
148
174
  end
149
175
 
150
176
  end