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.
@@ -0,0 +1,300 @@
1
+ #
2
+ # lib/porolog/instantiation.rb - Plain Old Ruby Objects Prolog Engine -- Instantiation
3
+ #
4
+ # Luis Esteban 2 May 2018
5
+ # created
6
+ #
7
+
8
+ module Porolog
9
+
10
+ # A Porolog::Instantiation implements an instantiation of a Variable of a Goal.
11
+ # An Instantiation joins two expressions. At least one expression must have a variable.
12
+ # An index may be provided to index into the value of either expression, or both may have an index.
13
+ # Thus, an element of each list could be instantiated without reference to the other elements:
14
+ # x = [a,b,c,d,e]
15
+ # |
16
+ # y = [p,q,r,s]
17
+ #
18
+ # @author Luis Esteban
19
+ #
20
+ # @!attribute variable1
21
+ # @return [Variable,Object] one end of the instantiation.
22
+ # @!attribute variable2
23
+ # @return [Variable,Object] the other end of the instantiation.
24
+ # @!attribute index1
25
+ # @return [Object,nil] the index into the value of variable1.
26
+ # @!attribute index2
27
+ # @return [Object,nil] the index into the value of variable2.
28
+ #
29
+ class Instantiation
30
+
31
+ # Error class for rescuing or detecting any Instantiation error.
32
+ class Error < PorologError ; end
33
+ # Error class indicating that an instantiation was created without any Variables.
34
+ class NoVariableError < Error ; end
35
+ # Error class indicating that an index could not be used although requested.
36
+ class UnhandledIndexError < Error ; end
37
+
38
+ attr_accessor :variable1, :variable2, :index1, :index2
39
+
40
+ # Clears all instantiations.
41
+ # @return [Boolean] success
42
+ def self.reset
43
+ @@instantiations = {}
44
+ true
45
+ end
46
+
47
+ reset
48
+
49
+ # @return [Hash{Array => Porolog::Instantiation}] all Instantiations
50
+ def self.instantiations
51
+ @@instantiations
52
+ end
53
+
54
+ # Finds or creates a new instantiation. At least one of the variables must be a Variable.
55
+ # @param variable1 [Porolog::Variable,Porolog::Value,Object] one end of the instantiation to be made.
56
+ # @param variable2 [Porolog::Variable,Porolog::Value,Object] the other end of the instantiation to be made.
57
+ # @param index1 [Integer,Symbol,nil] index into the value of variable1.
58
+ # @param index2 [Integer,Symbol,nil] index into the value of variable2.
59
+ # @return [Porolog::Instantiation] the found or created Instantiation.
60
+ def self.new(variable1, index1, variable2, index2)
61
+ raise NoVariableError, "Cannot instantiate non-variables: #{variable1.inspect} and #{variable2.inspect}" unless variable1.is_a?(Variable) || variable2.is_a?(Variable)
62
+
63
+ variable1 = Value.new variable1, variable2.goal unless variable1.is_a?(Variable) || variable1.is_a?(Value)
64
+ variable2 = Value.new variable2, variable1.goal unless variable2.is_a?(Variable) || variable2.is_a?(Value)
65
+
66
+ instantiation = \
67
+ @@instantiations[[variable1, index1, variable2, index2]] ||
68
+ @@instantiations[[variable2, index2, variable1, index1]] ||
69
+ super
70
+
71
+ instantiation
72
+ end
73
+
74
+ # Initializes and registers a new Instantiation. At least one of the variables must be a Variable.
75
+ # @param variable1 [Porolog::Variable,Porolog::Value,Object] one end of the instantiation to be made.
76
+ # @param variable2 [Porolog::Variable,Porolog::Value,Object] the other end of the instantiation to be made.
77
+ # @param index1 [Integer,Symbol,nil] index into the value of variable1.
78
+ # @param index2 [Integer,Symbol,nil] index into the value of variable2.
79
+ # @return [Porolog::Instantiation] the found or created Instantiation.
80
+ def initialize(variable1, index1, variable2, index2)
81
+ @variable1 = variable1
82
+ @variable2 = variable2
83
+ @index1 = index1
84
+ @index2 = index2
85
+
86
+ @variable1.instantiations << self
87
+ @variable2.instantiations << self
88
+
89
+ @@instantiations[signature] = self
90
+ end
91
+
92
+ # @return [Array] the signature of the Instantiation, which aids in avoiding duplication instantiations.
93
+ def signature
94
+ @signature ||= [@variable1, @index1, @variable2, @index2].freeze
95
+ @signature
96
+ end
97
+
98
+ # @return [String] pretty representation.
99
+ def inspect(indent = 0)
100
+ index1 = @index1 ? "[#{@index1.inspect}]" : ''
101
+ index2 = @index2 ? "[#{@index2.inspect}]" : ''
102
+
103
+ "#{' ' * indent}#{@variable1.inspect}#{index1} = #{@variable2.inspect}#{index2}"
104
+ end
105
+
106
+ # Removes itself from its variables' Instantiations and unregisters itself.
107
+ # @return [Boolean] success
108
+ def remove
109
+ @variable1.instantiations.delete(self) if @variable1.is_a?(Variable) || @variable1.is_a?(Value)
110
+ @variable2.instantiations.delete(self) if @variable2.is_a?(Variable) || @variable2.is_a?(Value)
111
+
112
+ @deleted = true
113
+ @@instantiations.delete(signature)
114
+ @deleted
115
+ end
116
+
117
+ # Returns the Goal of the other Variable to the one provided.
118
+ # @param variable [Porolog::Variable] the provided Variable.
119
+ # @return [Porolog::Goal,nil] the Goal of the other Variable.
120
+ def other_goal_to(variable)
121
+ return nil unless variables.include?(variable)
122
+
123
+ other_variable = (variables - [variable]).first
124
+ other_variable && other_variable.goal
125
+ end
126
+
127
+ # @return [Array<Porolog::Goal>] the Goals of the Variables of the Instantiation.
128
+ def goals
129
+ [@variable1.goal,@variable2.goal]
130
+ end
131
+
132
+ # @return [Array<Porolog::Variable,Object>] the Variables of the Instantiation.
133
+ def variables
134
+ [@variable1,@variable2]
135
+ end
136
+
137
+ # @param visited [Array] prevents infinite recursion.
138
+ # @return [Array] the values of the Variables of the Instantiation.
139
+ def values(visited = [])
140
+ return [] if visited.include?(self)
141
+
142
+ vv1 = values_for(@variable1, visited)
143
+ vv2 = values_for(@variable2, visited)
144
+
145
+ (vv1 + vv2).uniq
146
+ end
147
+
148
+ # @param value [Object] the provided value.
149
+ # @param index [Integer,Symbol,Array<Integer>] the index.
150
+ # @param visited [Array] prevents infinite recursion.
151
+ # @return [Object] the indexed value of the provided value.
152
+ def value_indexed(value, index, visited = [])
153
+ return nil unless value
154
+ return nil if value.type == :variable
155
+ return nil if value == [] && index == :tail
156
+
157
+ visit = [value, index]
158
+ return nil if visited.include?(visit)
159
+ visited = visited + [visit]
160
+
161
+ case index
162
+ when Integer
163
+ if value.respond_to?(:last) && value.last == UNKNOWN_TAIL
164
+ if index >= value.length - 1
165
+ UNKNOWN_TAIL
166
+ else
167
+ value[index]
168
+ end
169
+ elsif value.is_a?(Numeric)
170
+ value
171
+ else
172
+ value[index]
173
+ end
174
+
175
+ when Symbol
176
+ value_value = value.value(visited)
177
+ if value_value.respond_to?(index)
178
+ value_value.send(index)
179
+ else
180
+ value
181
+ end
182
+
183
+ when Array
184
+ if index.empty?
185
+ value[1..-1]
186
+ else
187
+ value[0...index.first]
188
+ end
189
+
190
+ else
191
+ if index
192
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} of #{value.inspect}"
193
+ else
194
+ value
195
+ end
196
+ end
197
+ end
198
+
199
+ # @param variable [Porolog::Variable,Porolog::Value] the specified variable (or end of the Instantiation).
200
+ # @param visited [Array] prevents infinite recursion.
201
+ # @return [Array<Object>] the values for the specified variable by finding the values of the other variable (i.e. the other end) and applying any indexes.
202
+ def values_for(variable, visited = [])
203
+ return [] if visited.include?(self)
204
+ visited = visited + [self]
205
+
206
+ if variable == @variable1
207
+ if @index1
208
+ [value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1)]
209
+ else
210
+ if @variable2.is_a?(Variable)
211
+ [value_indexed(@variable2.value(visited), @index2, visited)]
212
+ else
213
+ [value_indexed(@variable2, @index2, visited)]
214
+ end
215
+ end
216
+ elsif variable == @variable2
217
+ if @index2
218
+ [value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2)]
219
+ else
220
+ if @variable1.is_a?(Variable)
221
+ [value_indexed(@variable1.value(visited), @index1, visited)]
222
+ else
223
+ [value_indexed(@variable1, @index1, visited)]
224
+ end
225
+ end
226
+ else
227
+ []
228
+ end.compact
229
+ end
230
+
231
+ # @param value [Object] the known value.
232
+ # @param index [Integer,Symbol,Array] the known index.
233
+ # @return [Array] an Array where the known value is at the known index.
234
+ def value_at_index(value, index)
235
+ value && case index
236
+ when Integer
237
+ result = []
238
+ result[index] = value
239
+ result << UNKNOWN_TAIL
240
+ result
241
+
242
+ when Symbol
243
+ case index
244
+ when :head
245
+ [value, UNKNOWN_TAIL]
246
+ when :tail
247
+ [nil, *value]
248
+ else
249
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} for #{value.inspect}"
250
+ end
251
+
252
+ when Array
253
+ if index.empty?
254
+ [nil, *value]
255
+ else
256
+ #result = []
257
+ #result[0..index.first] = value
258
+ #result
259
+ #[*([*value][0..index.first]), UNKNOWN_TAIL]
260
+ [value, UNKNOWN_TAIL]
261
+ end
262
+
263
+ else
264
+ if index
265
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} for #{value.inspect}"
266
+ else
267
+ value
268
+ end
269
+ end
270
+ end
271
+
272
+ # @param variable [Porolog::Variable,Porolog::Value] the specified variable.
273
+ # @return [Boolean] whether the specified variable is not indexed.
274
+ def without_index_on?(variable)
275
+ [
276
+ [@variable1,@index1],
277
+ [@variable2,@index2],
278
+ ].any?{|pair|
279
+ pair.first == variable && pair.last.nil?
280
+ }
281
+ end
282
+
283
+ # @return [Boolean] whether the Instantiation has been deleted (memoized).
284
+ def deleted?
285
+ @deleted ||= @variable1.goal.deleted? || @variable2.goal.deleted?
286
+ @deleted
287
+ end
288
+
289
+ # @param goal [Porolog::Goal] the provided Goal.
290
+ # @return [Boolean] whether the Instantiation attaches to a variable in the provided Goal.
291
+ def belongs_to?(goal)
292
+ [
293
+ @variable1.goal,
294
+ @variable2.goal,
295
+ ].include?(goal)
296
+ end
297
+
298
+ end
299
+
300
+ end
@@ -10,17 +10,21 @@ module Porolog
10
10
  # A Porolog::Predicate corresponds to a Prolog predicate.
11
11
  # It contains rules (Porolog::Rule) and belongs to a Porolog::Scope.
12
12
  # When provided with arguments, it produces a Porolog::Arguments.
13
+ #
13
14
  # @author Luis Esteban
15
+ #
14
16
  # @!attribute [r] name
15
17
  # Name of the Predicate.
18
+ #
16
19
  # @!attribute [r] rules
17
20
  # Rules of the Predicate.
21
+ #
18
22
  class Predicate
19
23
 
20
24
  # Error class for rescuing or detecting any Predicate error.
21
- class PredicateError < PorologError ; end
25
+ class Error < PorologError ; end
22
26
  # Error class indicating an error with the name of a Predicate.
23
- class NameError < PredicateError ; end
27
+ class NameError < Error ; end
24
28
 
25
29
  attr_reader :name, :rules
26
30
 
@@ -40,9 +44,7 @@ module Porolog
40
44
  # @param scope_name [Object] the name (or otherwise object) used to register a scope.
41
45
  # @return [Porolog::Scope] the current Scope.
42
46
  def self.scope=(scope_name)
43
- if scope_name
44
- @@current_scope = Scope[scope_name] || Scope.new(scope_name)
45
- end
47
+ @@current_scope = Scope[scope_name] || Scope.new(scope_name) if scope_name
46
48
  @@current_scope
47
49
  end
48
50
 
@@ -69,7 +71,7 @@ module Porolog
69
71
  @name = name.to_sym
70
72
  @rules = []
71
73
 
72
- raise NameError.new("Cannot name a predicate 'predicate'") if @name == :predicate
74
+ raise NameError, "Cannot name a predicate 'predicate'" if @name == :predicate
73
75
 
74
76
  scope = Scope[scope_name] || Scope.new(scope_name)
75
77
  scope[@name] = self
@@ -83,14 +85,17 @@ module Porolog
83
85
  end
84
86
 
85
87
  # Create Arguments for the Predicate.
86
- # Provides the syntax options:
87
- # * p.(x,y,z)
88
+ # Provides the syntax option:
89
+ # p.(x,y,z)
90
+ # for
91
+ # p.arguments(x,y,z)
88
92
  # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
89
93
  def call(*args)
90
94
  Arguments.new(self,args)
91
95
  end
92
96
 
93
97
  # Create Arguments for the Predicate.
98
+ # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
94
99
  def arguments(*args)
95
100
  Arguments.new(self,args)
96
101
  end
@@ -106,18 +111,13 @@ module Porolog
106
111
  # A pretty print String of the Predicate.
107
112
  # @return [String] the Predicate as a String.
108
113
  def inspect
109
- lines = []
110
-
111
114
  if @rules.size == 1
112
- lines << "#{@name}:-#{@rules.first.inspect}"
115
+ "#{@name}:-#{@rules.first.inspect}"
113
116
  else
114
- lines << "#{@name}:-"
115
- @rules.each do |rule|
117
+ @rules.each_with_object(["#{@name}:-"]) do |rule, lines|
116
118
  lines << rule.inspect
117
- end
119
+ end.join("\n")
118
120
  end
119
-
120
- lines.join("\n")
121
121
  end
122
122
 
123
123
  # Return a builtin Predicate based on its key.
@@ -130,6 +130,23 @@ module Porolog
130
130
  self.new("_#{key}_#{@builtin_predicate_ids[key]}")
131
131
  end
132
132
 
133
+ # Satisfy the Predicate within the supplied Goal.
134
+ # Satisfy of each rule of the Predicate is called with the Goal and success block.
135
+ # @param goal [Porolog::Goal] the Goal within which to satisfy the Predicate.
136
+ # @param block [Proc] the block to be called each time a Rule of the Predicate is satisfied.
137
+ # @return [Boolean] whether any Rule was satisfied.
138
+ 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)
144
+ end
145
+ break if goal.terminated?
146
+ end
147
+ satisfied
148
+ end
149
+
133
150
  end
134
151
 
135
152
  end
@@ -8,17 +8,25 @@
8
8
  module Porolog
9
9
 
10
10
  # A Porolog::Rule is one clause of a Porolog::Predicate.
11
+ #
11
12
  # @author Luis Esteban
13
+ #
12
14
  # @!attribute [r] arguments
13
15
  # The Arguments of the Predicate for which this Rule applies.
14
16
  # @!attribute [r] definition
15
17
  # The definition of the Rule.
18
+ #
16
19
  class Rule
17
20
 
21
+ # Error class for rescuing or detecting any Rule error.
22
+ class Error < PorologError ; end
23
+ # Error class indicating that there is an error in the definition of the Rule.
24
+ class DefinitionError < Error ; end
25
+
18
26
  attr_reader :arguments, :definition
19
27
 
20
28
  # Clears all Rules.
21
- # @return [true]
29
+ # @return [Boolean] success
22
30
  def self.reset
23
31
  @@rules = []
24
32
  true
@@ -41,11 +49,130 @@ module Porolog
41
49
  "Rule#{(@@rules.index(self) || -1000) + 1}"
42
50
  end
43
51
 
44
- # @return [String] pretty representation
52
+ # @return [String] pretty representation.
45
53
  def inspect
46
54
  " #{@arguments.inspect}:- #{@definition.inspect}"
47
55
  end
48
56
 
57
+ # Try to satisfy the Rule for the given Goal.
58
+ # @param goal [Porolog::Goal] the Goal in which to satisfy this Rule.
59
+ # @param block [Proc] code to perform when the Rule is satisfied.
60
+ # @return [Boolean] the success of deleting the subgoal.
61
+ def satisfy(goal, &block)
62
+ subgoal = Goal.new self.arguments, goal
63
+
64
+ unified_goals = unify_goals(goal, subgoal)
65
+ if unified_goals
66
+ satisfy_definition(goal, subgoal) do |solution_goal|
67
+ block.call(solution_goal)
68
+ end
69
+ else
70
+ subgoal.log << "Dead-end: Cannot unify with #{goal.inspect}"
71
+ end
72
+
73
+ subgoal.delete!
74
+ end
75
+
76
+ # Try to satisfy the definition of the Rule for a given Goal.
77
+ # @param goal [Porolog::Goal] the given Goal for the Rule.
78
+ # @param subgoal [Porolog::Goal] the subgoal for the Rule's definition.
79
+ # @param block [Proc] code to perform when the definition is satisfied.
80
+ # @return [Boolean] whether the definition was satisfied.
81
+ def satisfy_definition(goal, subgoal, &block)
82
+ case @definition
83
+ when TrueClass
84
+ block.call(subgoal)
85
+ true
86
+
87
+ when FalseClass
88
+ false
89
+
90
+ when Array
91
+ satisfy_conjunction(goal, subgoal, @definition) do |solution_goal|
92
+ block.call(solution_goal)
93
+ end
94
+
95
+ else
96
+ raise DefinitionError, "UNEXPECTED TYPE OF DEFINITION: #{@definition.inspect} (#{@definition.class})"
97
+ end
98
+ end
99
+
100
+ # Try to satisfy the conjunction of the definition of the Rule for a given Goal.
101
+ # A conjunction is a sequence of expressions where the sequence is true
102
+ # if all the expressions are true.
103
+ #
104
+ # @param goal [Porolog::Goal] the given Goal for the Rule.
105
+ # @param subgoal [Porolog::Goal] the subgoal for the Rule's definition.
106
+ # @param conjunction [Array] the conjunction to satisfy.
107
+ # @param block [Proc] code to perform when the definition is satisfied.
108
+ # @return [Boolean] whether the definition was satisfied.
109
+ def satisfy_conjunction(goal, subgoal, conjunction, &block)
110
+ arguments = conjunction.first
111
+ conjunction = conjunction[1..-1]
112
+
113
+ # -- Handle non-Arguments --
114
+ case arguments
115
+ when :CUT, true
116
+ subgoal.log << "CUTTING #{goal.inspect}..."
117
+ result = true
118
+ if conjunction.empty?
119
+ block.call(subgoal)
120
+ else
121
+ result = satisfy_conjunction(goal, subgoal, conjunction, &block)
122
+ end
123
+
124
+ if arguments == :CUT
125
+ subgoal.calling_goal.terminate!
126
+ subgoal.calling_goal.log << "TERMINATED by #{subgoal.inspect}"
127
+ end
128
+
129
+ return result
130
+
131
+ when false
132
+ return false
133
+ end
134
+
135
+ # -- Unify Subgoal --
136
+ subsubgoal = arguments.goal(subgoal)
137
+ variables = (subgoal.arguments.variables + subsubgoal.arguments.variables).map(&:to_sym).uniq
138
+ unified = true
139
+ unifications = []
140
+
141
+ variables.each do |variable|
142
+ name = variable
143
+
144
+ unification = unify(name, name, subgoal, subsubgoal)
145
+ unified &&= !!unification
146
+ if unified
147
+ unifications += unification
148
+ else
149
+ #:nocov:
150
+ subsubgoal.log << "Couldn't unify: #{name.inspect} WITH #{subgoal.myid} AND #{subsubgoal.myid}"
151
+ break
152
+ #:nocov:
153
+ end
154
+ end
155
+
156
+ # -- Instantiate Unifications --
157
+ unified &&= instantiate_unifications(unifications) if unified
158
+
159
+ # -- Satisfy Subgoal --
160
+ result = false
161
+ unified && subsubgoal.satisfy() do
162
+ result = true
163
+ if conjunction.empty?
164
+ block.call(goal)
165
+ else
166
+ result = satisfy_conjunction(goal, subsubgoal, conjunction, &block)
167
+ end
168
+ end
169
+
170
+ # -- Uninstantiate --
171
+ subsubgoal.delete!
172
+
173
+ result
174
+ end
175
+
49
176
  end
50
177
 
51
178
  end