porolog 0.0.7 → 0.0.8

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.
@@ -11,7 +11,9 @@ module Porolog
11
11
  # A predicate is not like a subroutine, although there are many similarities.
12
12
  # An Arguments represents an instance of a predicate with a specific set of arguments.
13
13
  # This forms the basis for implementing a goal to solve the predicate with those specific arguments.
14
+ #
14
15
  # @author Luis Esteban
16
+ #
15
17
  # @!attribute [r] predicate
16
18
  # The Predicate for which these are the arguments.
17
19
  # @!attribute [r] arguments
@@ -52,7 +54,7 @@ module Porolog
52
54
 
53
55
  # @return [String] pretty representation
54
56
  def inspect
55
- "#{@predicate.name}(#{@arguments && @arguments.map(&:inspect).join(',')})"
57
+ "#{@predicate && @predicate.name}(#{@arguments && @arguments.map(&:inspect).join(',')})"
56
58
  end
57
59
 
58
60
  # Creates a fact rule that states that these arguments satisfy the Predicate.
@@ -114,18 +116,18 @@ module Porolog
114
116
  end
115
117
 
116
118
  # Returns memoized solutions
117
- # @param number [Integer] the maximum number of solutions to find (nil means find all)
119
+ # @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
118
120
  # @return [Array<Hash{Symbol => Object}>] the solutions found (memoized)
119
- def solutions(number = nil)
120
- @solutions ||= solve(number)
121
+ def solutions(max_solutions = nil)
122
+ @solutions ||= solve(max_solutions)
121
123
  @solutions
122
124
  end
123
125
 
124
126
  # Solves the Arguments
125
- # @param number_of_solutions [Integer] the maximum number of solutions to find (nil means find all)
127
+ # @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
126
128
  # @return [Array<Hash{Symbol => Object}>] the solutions found
127
- def solve(number_of_solutions = nil)
128
- @solutions = goal.solve(number_of_solutions)
129
+ def solve(max_solutions = nil)
130
+ @solutions = goal.solve(max_solutions)
129
131
  end
130
132
 
131
133
  # Extracts solution values.
@@ -141,7 +143,12 @@ module Porolog
141
143
  def solve_for(*variables)
142
144
  variables = [*variables]
143
145
  solutions.map{|solution|
144
- variables.map{|variable| solution[variable] }
146
+ values = variables.map{|variable| solution[variable] }
147
+ if values.size == 1
148
+ values.first
149
+ else
150
+ values
151
+ end
145
152
  }
146
153
  end
147
154
 
@@ -154,12 +161,7 @@ module Porolog
154
161
  # @param goal [Porolog::Goal] the destination goal
155
162
  # @return [Porolog::Arguments] the duplicated Arguments
156
163
  def dup(goal)
157
- arguments_dup = arguments.dup
158
- # TODO: Uncomment when HeadTail added
159
- #(0...arguments_dup.size).each do |i|
160
- # arguments_dup[i] = arguments_dup[i].dup(goal) if arguments_dup[i].is_a?(HeadTail)
161
- #end
162
- self.class.new @predicate, arguments_dup
164
+ self.class.new @predicate, goal.variablise(arguments)
163
165
  end
164
166
 
165
167
  # @param other [Porolog::Arguments] arguments for comparison
@@ -41,6 +41,7 @@ class Object
41
41
  [self]/other
42
42
  end
43
43
 
44
+ # Syntactic sugar to look like Prolog's [H|T]
44
45
  alias / :tail
45
46
 
46
47
  # @return [Boolean] whether the Object is an Array with a head and a tail (for an Object, should be false).
@@ -161,4 +162,9 @@ class Array
161
162
  length == 2 && (last.is_a?(Tail) || last == UNKNOWN_TAIL)
162
163
  end
163
164
 
165
+ # @return [Porolog::Goal] the goal that is most likely to be the goal for this array.
166
+ def goal
167
+ map{|element| element.goal if element.respond_to?(:goal) }.flatten.find{|goal| goal.is_a?(Porolog::Goal) }
168
+ end
169
+
164
170
  end
@@ -8,6 +8,12 @@
8
8
  module Porolog
9
9
 
10
10
  # Error class to enable rescuing or detecting any Porolog error.
11
+ #
12
+ # @author Luis Esteban
13
+ #
11
14
  class PorologError < RuntimeError ; end
15
+
16
+ # Error indicating that a Goal is needed but could not be found.
17
+ class NoGoalError < PorologError ; end
12
18
 
13
19
  end
@@ -8,60 +8,85 @@
8
8
  module Porolog
9
9
 
10
10
  # A Porolog::Goal finds solutions for specific Arguments of a Predicate.
11
+ #
11
12
  # @author Luis Esteban
13
+ #
12
14
  # @!attribute calling_goal
13
- # @return [Porolog::Goal] The parent goal if this goal is a subgoal.
15
+ # @return [Porolog::Goal, nil] The parent goal if this goal is a subgoal, otherwise nil.
14
16
  # @!attribute arguments
15
17
  # @return [Porolog::Arguments] The specific Arguments this goal is trying to solve.
16
- # @!attribute terminate
17
- # @return [Boolean] Whether this Goal is to terminate.
18
18
  # @!attribute index
19
- # @return [Integer, NilClass] Solution index for builtin predicates.
19
+ # @return [Integer, nil] Solution index for builtin predicates.
20
20
  # @!attribute result
21
21
  # @return [Boolean] Whether any result has been found.
22
+ # @!attribute description
23
+ # @return [String] Description of the Arguments the Goal is attempting to solve.
24
+ # @!attribute log
25
+ # @return [Array<String>] Log of events, namely unification failures.
26
+ #
22
27
  class Goal
23
28
 
24
29
  # Error class for rescuing or detecting any Goal error.
25
- class GoalError < PorologError ; end
30
+ class Error < PorologError ; end
26
31
  # Error class indicating an unexpected type of Rule definition.
27
- class DefinitionError < GoalError ; end
32
+ class DefinitionError < Error ; end
28
33
  # Error class indicating a non-Variable was attempted to be instantiated.
29
- class NotVariableError < GoalError ; end
34
+ class NotVariableError < Error ; end
30
35
 
31
36
  # Clears all goals and ensures they are deleted.
32
- # @return [true]
37
+ # @return [Boolean] success
33
38
  def self.reset
34
39
  @@goals ||= []
35
40
  goals = @@goals
36
41
  @@goals = []
37
42
  goals.map(&:deleted?)
38
- @@cache = {}
39
43
  true
40
44
  end
41
45
 
42
46
  reset
43
47
 
44
- attr_accessor :calling_goal, :arguments, :terminate, :index, :result
48
+ attr_accessor :calling_goal, :arguments, :index, :result, :description, :log
45
49
 
46
50
  # Initializes and registers the Goal.
47
51
  # @param arguments [Porolog::Arguments] the Predicate Arguments that this Goal is to solve.
48
52
  # @param calling_goal [Porolog::Goal] the parent Goal if this Goal is a subgoal.
49
53
  def initialize(arguments, calling_goal = nil)
54
+ @@goals << self
55
+
50
56
  @arguments = arguments
51
57
  @terminate = false
52
58
  @calling_goal = calling_goal
53
59
  @variables = {}
54
- @result = false
60
+ @values = {}
61
+ @result = nil
62
+ @description = "Solve #{arguments.inspect}"
63
+ @log = []
55
64
 
56
65
  @arguments = @arguments.dup(self) if @arguments.is_a?(Arguments)
66
+ @solutions = []
67
+ end
68
+
69
+ # @return [Array<Porolog::Goal>] the calling chain for this goal.
70
+ def ancestors
71
+ ancestors = []
72
+ goal = self
57
73
 
58
- @@goals << self
74
+ while goal
75
+ ancestors << goal
76
+ goal = goal.calling_goal
77
+ end
78
+
79
+ ancestors.reverse
59
80
  end
60
81
 
61
82
  # A convenience method for testing/debugging.
62
- # @return [Array<Porolog::Goal>]
63
- def self.goals
64
- @@goals
83
+ # @return [String] the calling chain for this goal as a String.
84
+ def ancestry
85
+ indent = -1
86
+ ancestors.map do |goal|
87
+ indent += 1
88
+ "#{' ' * indent}#{goal.myid} -- #{goal.description} #{goal.variables.inspect}"
89
+ end.join("\n")
65
90
  end
66
91
 
67
92
  # A convenience method for testing/debugging.
@@ -70,42 +95,125 @@ module Porolog
70
95
  "Goal#{(@@goals.index(self) || -1000) + 1}"
71
96
  end
72
97
 
98
+ # @return [String] pretty representation.
99
+ def inspect
100
+ "#{myid} -- #{description}"
101
+ end
102
+
103
+ # A convenience method for testing/debugging.
104
+ # @return [Array<Porolog::Goal>]
105
+ def self.goals
106
+ @@goals
107
+ end
108
+
109
+ # Delete the Goal
110
+ # @return [Boolean] whether the Goal has been deleted (memoized)
111
+ def delete!
112
+ @@goals.delete(self)
113
+ deleted?
114
+ end
115
+
73
116
  # @return [Boolean] whether the Goal has been deleted (memoized)
74
117
  def deleted?
75
118
  @deleted ||= check_deleted
76
119
  @deleted
77
120
  end
78
121
 
79
- # Determines whether the Goal has been deleted and removes its Variables if it has
122
+ # Determines whether the Goal has been deleted and removes its Variables and Values if it has.
80
123
  # @return [Boolean] whether the Goal has been deleted
81
124
  def check_deleted
82
- if @@goals.include?(self)
83
- false
84
- else
85
- @variables.delete_if do |name,variable|
86
- variable.remove
87
- true
88
- end
125
+ return false if @@goals.include?(self)
126
+
127
+ @variables.delete_if do |name,variable|
128
+ variable.remove
89
129
  true
90
130
  end
131
+
132
+ @values.delete_if do |name,value|
133
+ value.remove
134
+ true
135
+ end
136
+
137
+ true
138
+ end
139
+
140
+ # Sets that the goal should no longer search any further rules for the current predicate
141
+ # once the current rule has finished being evaluated.
142
+ # This method implements the :CUT rule.
143
+ # That is, backtracking does not go back past the point of the cut.
144
+ # @return [Boolean] true.
145
+ def terminate!
146
+ @log << 'terminating'
147
+ @terminate = true
148
+ end
149
+
150
+ # @return [Boolean] whether the goal has been cut.
151
+ def terminated?
152
+ @terminate
153
+ end
154
+
155
+ # Converts all embedded Symbols to Variables within this Goal.
156
+ # @param object [Object] the object to variablise.
157
+ # @return [Object] the variablised object.
158
+ def variablise(object)
159
+ case object
160
+ when Symbol,Variable
161
+ variable(object)
162
+ when Array
163
+ object.map{|element| variablise(element) }
164
+ when Arguments
165
+ object.dup(self)
166
+ when Tail
167
+ Tail.new variablise(object.value)
168
+ when Value
169
+ object
170
+ when NilClass
171
+ nil
172
+ else
173
+ if object == UNKNOWN_TAIL || object == UNKNOWN_ARRAY
174
+ object
175
+ else
176
+ value(object)
177
+ end
178
+ end
91
179
  end
92
180
 
181
+ alias_method :[], :variablise
182
+
93
183
  # @return [Hash{Symbol => Object}] the Variables and their current values of this Goal
94
184
  def variables
95
185
  @variables.keys.each_with_object({}){|variable,variable_list|
96
- next if self.variable(variable).internal?
97
- variable_list[variable] = self.value_of(variable)
186
+ value = value_of(variable)
187
+ value = value.value if value.is_a?(Value)
188
+ if value.is_a?(Variable)
189
+ variable_list[variable] = nil
190
+ elsif value.is_a?(Value) || value.is_a?(Array)
191
+ variable_list[variable] = value.value
192
+ else
193
+ variable_list[variable] = value
194
+ end
98
195
  }
99
196
  end
100
197
 
198
+ # A convenience method for testing/debugging.
199
+ # @return [String] a tree representation of all the instantiations of this goal's variables.
200
+ def inspect_variables
201
+ @variables.map{|name,variable|
202
+ variable.inspect_with_instantiations
203
+ }.join("\n")
204
+ end
205
+
206
+ # A convenience method for testing/debugging.
207
+ # @return [Array<Object>] the values instantiated for this goal.
208
+ def values
209
+ @values.map{|name,value| value.value }
210
+ end
211
+
101
212
  # Finds or tries to create a variable in the goal (as much as possible) otherwise passes the parameter back.
102
213
  # @param name [Object] name of variable or variable
103
214
  # @return [Porolog::Variable, Object] a variable of the goal or the original parameter
104
215
  def variable(name)
105
- unless name.is_a?(Symbol) || name.is_a?(Variable)
106
- # -- Return parameter if it is not a variable --
107
- return name
108
- end
216
+ return name unless name.is_a?(Symbol) || name.is_a?(Variable)
109
217
 
110
218
  if name.is_a?(Variable)
111
219
  variable = name
@@ -117,60 +225,89 @@ module Porolog
117
225
  @variables[name]
118
226
  end
119
227
 
228
+ # @param value [Object] the value that needs to be associated with this Goal.
229
+ # @return [Porolog::Value] a Value, ensuring it has been associated with this Goal so that it can be uninstantiated.
230
+ def value(value)
231
+ @values[value] ||= Value.new(value, self)
232
+ @values[value]
233
+ end
234
+
120
235
  # Returns the value of the indicated variable or value
121
236
  # @param name [Symbol, Object] name of the variable or value
122
- # @param _index [Object] index is not used, it is for polymorphic compatibility with other classes
237
+ # @param index [Object] index is not yet used
123
238
  # @param visited [Array] used to avoid infinite recursion
124
239
  # @return [Object] the value
125
- def value_of(name, _index = nil, visited = [])
126
- if name.is_a?(Symbol)
127
- variable = @variables[name]
128
- value = variable && variable.value(visited) || self.variable(name)
129
- else
130
- value = name
131
- end
132
-
133
- if value.respond_to?(:value)
134
- begin
135
- value = value.value(visited)
136
- rescue
137
- return value
138
- end
240
+ def value_of(name, index = nil, visited = [])
241
+ variable(name).value(visited)
242
+ end
243
+
244
+ # Returns the values of an Object. For most objects, this will basically just be the object.
245
+ # For Arrays, an Array will be returned where the elements are the value of the elements.
246
+ # @param object [Object] the Object to find the values of.
247
+ # @return [Object,Array<Object>] the value(s) of the object.
248
+ def values_of(object)
249
+ case object
250
+ when Array
251
+ object.map{|element| values_of(element) }
252
+ when Variable,Symbol,Value
253
+ value_of(object)
254
+ when Tail
255
+ # TODO: This needs to splat outwards; in the meantime, it splats just the first.
256
+ value_of(object.value).first
257
+ else
258
+ object
139
259
  end
140
-
141
- value
142
260
  end
143
261
 
144
262
  # Finds all possible solutions to the Predicate for the specific Arguments.
145
- # @param number_of_solutions [Integer] if provided, the search is stopped once the number of solutions has been found
263
+ # @param max_solutions [Integer] if provided, the search is stopped once the number of solutions has been found
146
264
  # @return [Array<Hash{Symbol => Object}>]
147
- def solve(number_of_solutions = nil)
148
- solutions = []
149
- variables = @arguments.variables
265
+ def solve(max_solutions = nil)
266
+ return @solutions unless @arguments
150
267
 
151
- external_variables = variables.reject{|variable|
152
- self.variable(variable).internal?
153
- }
268
+ predicate = @arguments.predicate
154
269
 
155
- satisfy do |goal|
156
- @result = true
157
- solution = {}
158
-
159
- external_variables.each{|variable|
160
- solution[variable.to_sym] = goal.value_of(variable)
161
- }
270
+ predicate && predicate.satisfy(self) do |goal|
271
+ @solutions << variables
272
+ @log << "SOLUTION: #{variables}"
273
+ @log << goal.ancestry
274
+ @log << goal.inspect_variables
162
275
 
163
- solutions << solution
164
-
165
- if number_of_solutions && solutions.length >= number_of_solutions
166
- @solutions = solutions
167
- return @solutions
168
- end
276
+ return @solutions if max_solutions && @solutions.size >= max_solutions
169
277
  end
170
278
 
171
- # TODO: Delete all descendent goals
279
+ @solutions
280
+ end
281
+
282
+ # Solves the goal but not as the root goal for a query.
283
+ # That is, solves an intermediate goal.
284
+ # @param block [Proc] code to perform when the Goal is satisfied.
285
+ # @return [Boolean] whether the definition was satisfied.
286
+ def satisfy(&block)
287
+ return false unless @arguments
288
+
289
+ predicate = @arguments.predicate
290
+
291
+ predicate && predicate.satisfy(self) do |subgoal|
292
+ block.call(subgoal)
293
+ end
294
+ end
295
+
296
+ # Instantiates a Variable to another Variable or Value, for this Goal.
297
+ # @param name [Symbol,Porolog::Variable,Object] the name that references the variable to be instantiated.
298
+ # @param other [Object] the value that the variable is to be instantiated to.
299
+ # @param other_goal [Porolog::Goal,nil] the Goal of the other value.
300
+ # @return [Porolog::Instantiation] the instantiation.
301
+ def instantiate(name, other, other_goal = self)
302
+ variable = self.variable(name)
303
+
304
+ other = if other.type == :variable
305
+ other_goal.variable(other)
306
+ else
307
+ other_goal.value(other)
308
+ end
172
309
 
173
- @solutions = solutions
310
+ variable.instantiate(other)
174
311
  end
175
312
 
176
313
  end