porolog 0.0.4 → 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.
- checksums.yaml +4 -4
- data/README.md +30 -5
- data/Rakefile +7 -2
- data/bin/porolog +58 -1
- data/coverage/badge.svg +1 -1
- data/coverage/index.html +76733 -2638
- data/doc/Array.html +1066 -0
- data/doc/Object.html +674 -0
- data/doc/Porolog.html +4153 -74
- data/doc/Symbol.html +501 -0
- data/doc/_index.html +280 -6
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +34 -39
- data/doc/index.html +34 -39
- data/doc/method_list.html +1337 -57
- data/doc/top-level-namespace.html +4 -2
- data/lib/porolog.rb +1144 -4
- data/lib/porolog/arguments.rb +28 -24
- data/lib/porolog/core_ext.rb +188 -0
- data/lib/porolog/error.rb +9 -0
- data/lib/porolog/goal.rb +357 -0
- data/lib/porolog/instantiation.rb +346 -0
- data/lib/porolog/predicate.rb +74 -31
- data/lib/porolog/predicate/builtin.rb +825 -0
- data/lib/porolog/rule.rb +162 -0
- data/lib/porolog/scope.rb +4 -4
- data/lib/porolog/tail.rb +57 -0
- data/lib/porolog/value.rb +105 -0
- data/lib/porolog/variable.rb +325 -0
- data/test/porolog/arguments_test.rb +244 -195
- data/test/porolog/core_ext_test.rb +290 -0
- data/test/porolog/goal_test.rb +891 -0
- data/test/porolog/instantiation_test.rb +910 -0
- data/test/porolog/porolog_test.rb +2376 -13
- data/test/porolog/predicate/builtin_test.rb +1340 -0
- data/test/porolog/predicate_test.rb +84 -30
- data/test/porolog/rule_test.rb +527 -0
- data/test/porolog/scope_test.rb +0 -2
- data/test/porolog/tail_test.rb +127 -0
- data/test/porolog/value_test.rb +315 -0
- data/test/porolog/variable_test.rb +1614 -0
- data/test/samples_test.rb +277 -0
- data/test/test_helper.rb +115 -0
- metadata +34 -7
data/lib/porolog/arguments.rb
CHANGED
@@ -11,14 +11,16 @@ 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
|
18
20
|
# The actual arguments.
|
19
21
|
class Arguments
|
20
22
|
|
21
|
-
attr_reader :predicate, :arguments
|
23
|
+
attr_reader :predicate, :arguments, :block
|
22
24
|
|
23
25
|
# Unregisters all Arguments
|
24
26
|
# @return [true]
|
@@ -32,9 +34,10 @@ module Porolog
|
|
32
34
|
# Creates a new Arguments for a Predicate
|
33
35
|
# @param predicate [Porolog::Predicate] the Predicate for which these are the arguments
|
34
36
|
# @param arguments [Array<Object>] the actual arguments
|
35
|
-
def initialize(predicate, arguments)
|
37
|
+
def initialize(predicate, arguments, &block)
|
36
38
|
@predicate = predicate
|
37
39
|
@arguments = arguments
|
40
|
+
@block = block
|
38
41
|
@@arguments << self
|
39
42
|
end
|
40
43
|
|
@@ -52,7 +55,8 @@ module Porolog
|
|
52
55
|
|
53
56
|
# @return [String] pretty representation
|
54
57
|
def inspect
|
55
|
-
"
|
58
|
+
block_inspect = block.nil? ? '' : "{#{block.inspect}}"
|
59
|
+
"#{@predicate&.name}(#{@arguments&.map(&:inspect).join(',')})#{block_inspect}"
|
56
60
|
end
|
57
61
|
|
58
62
|
# Creates a fact rule that states that these arguments satisfy the Predicate.
|
@@ -101,9 +105,7 @@ module Porolog
|
|
101
105
|
|
102
106
|
# @return [Array<Symbol>] the variables contained in the arguments
|
103
107
|
def variables
|
104
|
-
@arguments.map
|
105
|
-
argument.variables
|
106
|
-
}.flatten.uniq
|
108
|
+
@arguments.map(&:variables).flatten.uniq
|
107
109
|
end
|
108
110
|
|
109
111
|
# Creates a Goal for solving this Arguments for the Predicate
|
@@ -114,22 +116,23 @@ module Porolog
|
|
114
116
|
end
|
115
117
|
|
116
118
|
# Returns memoized solutions
|
117
|
-
# @param
|
118
|
-
# @return [Array<Hash>] the solutions found (memoized)
|
119
|
-
def solutions(
|
120
|
-
@solutions ||= solve(
|
121
|
-
@solutions
|
119
|
+
# @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
|
120
|
+
# @return [Array<Hash{Symbol => Object}>] the solutions found (memoized)
|
121
|
+
def solutions(max_solutions = nil)
|
122
|
+
@solutions ||= solve(max_solutions)
|
123
|
+
max_solutions && @solutions[0...max_solutions] || @solutions
|
122
124
|
end
|
123
125
|
|
124
126
|
# Solves the Arguments
|
125
|
-
# @param
|
126
|
-
# @return [Array<Hash>] the solutions found
|
127
|
-
def solve(
|
128
|
-
@solutions = goal.solve(
|
127
|
+
# @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
|
128
|
+
# @return [Array<Hash{Symbol => Object}>] the solutions found
|
129
|
+
def solve(max_solutions = nil)
|
130
|
+
@solutions = goal.solve(max_solutions)
|
129
131
|
end
|
130
132
|
|
131
133
|
# Extracts solution values.
|
132
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)
|
133
136
|
# @return [Array<Object>] all the values for the variables given
|
134
137
|
# @example
|
135
138
|
# predicate :treasure_at
|
@@ -138,27 +141,28 @@ module Porolog
|
|
138
141
|
# xs = arguments.solve_for(:X)
|
139
142
|
# ys = arguments.solve_for(:Y)
|
140
143
|
# coords = xs.zip(ys)
|
141
|
-
def solve_for(*variables)
|
144
|
+
def solve_for(*variables, max_solutions: nil)
|
142
145
|
variables = [*variables]
|
143
|
-
solutions.map{|solution|
|
144
|
-
variables.map{|variable| solution[variable] }
|
146
|
+
solutions(max_solutions).map{|solution|
|
147
|
+
values = variables.map{|variable| solution[variable] }
|
148
|
+
if values.size == 1
|
149
|
+
values.first
|
150
|
+
else
|
151
|
+
values
|
152
|
+
end
|
145
153
|
}
|
146
154
|
end
|
147
155
|
|
148
156
|
# @return [Boolean] whether any solutions were found
|
149
157
|
def valid?
|
150
|
-
!solutions.empty?
|
158
|
+
!solutions(1).empty?
|
151
159
|
end
|
152
160
|
|
153
161
|
# Duplicates the Arguments in the context of the given goal
|
154
162
|
# @param goal [Porolog::Goal] the destination goal
|
155
163
|
# @return [Porolog::Arguments] the duplicated Arguments
|
156
164
|
def dup(goal)
|
157
|
-
|
158
|
-
(0...arguments_dup.size).each do |i|
|
159
|
-
arguments_dup[i] = arguments_dup[i].dup(goal) if arguments_dup[i].is_a?(HeadTail)
|
160
|
-
end
|
161
|
-
self.class.new @predicate, arguments_dup
|
165
|
+
self.class.new @predicate, goal.variablise(arguments), &@block
|
162
166
|
end
|
163
167
|
|
164
168
|
# @param other [Porolog::Arguments] arguments for comparison
|
@@ -0,0 +1,188 @@
|
|
1
|
+
#
|
2
|
+
# lib/porolog/core_ext.rb - Plain Old Ruby Objects Prolog Engine -- Core Extensions
|
3
|
+
#
|
4
|
+
# Luis Esteban 2 May 2018
|
5
|
+
# created
|
6
|
+
#
|
7
|
+
# @author Luis Esteban
|
8
|
+
# Extends Core Ruy Classes
|
9
|
+
#
|
10
|
+
|
11
|
+
# In Porolog, Objects represent atomic values.
|
12
|
+
class Object
|
13
|
+
|
14
|
+
# A convenience method for testing/debugging.
|
15
|
+
# @return [String] the equivalent of inspect.
|
16
|
+
def myid
|
17
|
+
inspect
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Array] embedded variables (for an Object, should be none).
|
21
|
+
def variables
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Symbol] the type of the object (for an Object, should be :atomic)
|
26
|
+
def type
|
27
|
+
:atomic
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Object] the value of the object, which is itself.
|
31
|
+
def value(*)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param other [Object] the Object which is to be the tail
|
36
|
+
# @return [Array] an Array with the Object being the head and the other Object being the tail.
|
37
|
+
# @example
|
38
|
+
# 'head' / ['body', 'foot'] # ==> ['head', 'body', 'foot']
|
39
|
+
# 7.tail(:t) # ==> [7, *:t]
|
40
|
+
def tail(other)
|
41
|
+
[self]/other
|
42
|
+
end
|
43
|
+
|
44
|
+
# Syntactic sugar to look like Prolog's [H|T]
|
45
|
+
alias / :tail
|
46
|
+
|
47
|
+
# @return [Boolean] whether the Object is an Array with a head and a tail (for an Object, should be false).
|
48
|
+
def headtail?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# In Porolog, Symbols represent Variables.
|
56
|
+
class Symbol
|
57
|
+
|
58
|
+
# A convenience method for testing/debugging.
|
59
|
+
# @return [String] the equivalent of inspect.
|
60
|
+
def myid
|
61
|
+
inspect
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Array] embedded variables (for a Symbol, should be itself).
|
65
|
+
def variables
|
66
|
+
[self]
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Symbol] the type of the object (for a Symbol, should be :variable)
|
70
|
+
def type
|
71
|
+
:variable
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param other [Object] the Object which is to be the tail
|
75
|
+
# @return [Array] an Array with the Object being the head and the other Object being the tail.
|
76
|
+
def /(other)
|
77
|
+
[self]/other
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# In Porolog, Arrays are the equivalent of lists.
|
84
|
+
class Array
|
85
|
+
|
86
|
+
# @return [Array] embedded variables.
|
87
|
+
def variables
|
88
|
+
map(&:variables).flatten
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Array] the values of its elements.
|
92
|
+
def value(visited = [])
|
93
|
+
return self if visited.include?(self)
|
94
|
+
visited = visited + [self]
|
95
|
+
flat_map{|element|
|
96
|
+
if element.is_a?(Tail)
|
97
|
+
tail = element.value(visited)
|
98
|
+
if tail.is_a?(Array)
|
99
|
+
tail
|
100
|
+
elsif tail.is_a?(Variable) || tail.is_a?(Value)
|
101
|
+
tail = tail.value(visited)
|
102
|
+
if tail.is_a?(Array)
|
103
|
+
tail
|
104
|
+
elsif tail.is_a?(Variable) || tail.is_a?(Value)
|
105
|
+
tail = tail.goal.variablise(tail.value(visited))
|
106
|
+
if tail.is_a?(Array)
|
107
|
+
tail
|
108
|
+
else
|
109
|
+
[element]
|
110
|
+
end
|
111
|
+
else
|
112
|
+
[element]
|
113
|
+
end
|
114
|
+
else
|
115
|
+
[element]
|
116
|
+
end
|
117
|
+
else
|
118
|
+
[element.value(visited)]
|
119
|
+
end
|
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
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [Symbol] the type of the object (for an Array, should be :array)
|
140
|
+
def type
|
141
|
+
:array
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param other [Object] the Object which is to be the tail
|
145
|
+
# @return [Array] an Array with the Object being the head and the other Object being the tail.
|
146
|
+
def /(other)
|
147
|
+
if other.is_a?(Porolog::Variable) || other.is_a?(Symbol)
|
148
|
+
self + [Tail.new(other)]
|
149
|
+
else
|
150
|
+
self + [*other]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param head_size [Integer] specifies the size of the head
|
155
|
+
# @return [Object] the head of the Array
|
156
|
+
def head(head_size = 1)
|
157
|
+
if head_size == 1
|
158
|
+
if first == Porolog::UNKNOWN_TAIL
|
159
|
+
nil
|
160
|
+
else
|
161
|
+
first
|
162
|
+
end
|
163
|
+
else
|
164
|
+
self[0...head_size]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# @param head_size [Integer] specifies the size of the head
|
169
|
+
# @return [Object] the tail of the Array
|
170
|
+
def tail(head_size = 1)
|
171
|
+
if last == Porolog::UNKNOWN_TAIL
|
172
|
+
[*self[head_size..-2], Porolog::UNKNOWN_TAIL]
|
173
|
+
else
|
174
|
+
[*self[head_size..-1]]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [Boolean] whether the Object is an Array with a head and a tail.
|
179
|
+
def headtail?
|
180
|
+
length == 2 && (last.is_a?(Tail) || last == UNKNOWN_TAIL)
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [Porolog::Goal] the goal that is most likely to be the goal for this array.
|
184
|
+
def goal
|
185
|
+
map{|element| element.goal if element.respond_to?(:goal) }.flatten.find{|goal| goal.is_a?(Porolog::Goal) }
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
data/lib/porolog/error.rb
CHANGED
@@ -8,6 +8,15 @@
|
|
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
|
18
|
+
|
19
|
+
# Error indicating that a Variable is needed but was not provided.
|
20
|
+
class NonVariableError < PorologError ; end
|
12
21
|
|
13
22
|
end
|
data/lib/porolog/goal.rb
ADDED
@@ -0,0 +1,357 @@
|
|
1
|
+
#
|
2
|
+
# lib/porolog/goal.rb - Plain Old Ruby Objects Prolog Engine -- Goal
|
3
|
+
#
|
4
|
+
# Luis Esteban 2 May 2018
|
5
|
+
# created
|
6
|
+
#
|
7
|
+
|
8
|
+
module Porolog
|
9
|
+
|
10
|
+
# A Porolog::Goal finds solutions for specific Arguments of a Predicate.
|
11
|
+
#
|
12
|
+
# @author Luis Esteban
|
13
|
+
#
|
14
|
+
# @!attribute calling_goal
|
15
|
+
# @return [Porolog::Goal, nil] The parent goal if this goal is a subgoal, otherwise nil.
|
16
|
+
# @!attribute arguments
|
17
|
+
# @return [Porolog::Arguments] The specific Arguments this goal is trying to solve.
|
18
|
+
# @!attribute index
|
19
|
+
# @return [Integer, nil] Solution index for builtin predicates.
|
20
|
+
# @!attribute result
|
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
|
+
#
|
27
|
+
class Goal
|
28
|
+
|
29
|
+
# Error class for rescuing or detecting any Goal error.
|
30
|
+
class Error < PorologError ; end
|
31
|
+
# Error class indicating an unexpected type of Rule definition.
|
32
|
+
class DefinitionError < Error ; end
|
33
|
+
# Error class indicating a non-Variable was attempted to be instantiated.
|
34
|
+
class NotVariableError < Error ; end
|
35
|
+
|
36
|
+
# Clears all goals and ensures they are deleted.
|
37
|
+
# @return [Boolean] success
|
38
|
+
def self.reset
|
39
|
+
@@goals ||= []
|
40
|
+
goals = @@goals
|
41
|
+
@@goals = []
|
42
|
+
@@goal_count = 0
|
43
|
+
goals.map(&:deleted?)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Integer] the number of goals created
|
48
|
+
def self.goal_count
|
49
|
+
@@goal_count
|
50
|
+
end
|
51
|
+
|
52
|
+
reset
|
53
|
+
|
54
|
+
attr_accessor :calling_goal, :arguments, :index, :result, :description, :log
|
55
|
+
|
56
|
+
# Initializes and registers the Goal.
|
57
|
+
# @param arguments [Porolog::Arguments] the Predicate Arguments that this Goal is to solve.
|
58
|
+
# @param calling_goal [Porolog::Goal] the parent Goal if this Goal is a subgoal.
|
59
|
+
def initialize(arguments, calling_goal = nil)
|
60
|
+
@@goals << self
|
61
|
+
@@goal_count += 1
|
62
|
+
|
63
|
+
@arguments = arguments
|
64
|
+
@terminate = false
|
65
|
+
@calling_goal = calling_goal
|
66
|
+
@variables = {}
|
67
|
+
@values = {}
|
68
|
+
@result = nil
|
69
|
+
@description = "Solve #{arguments.inspect}"
|
70
|
+
@log = []
|
71
|
+
|
72
|
+
@arguments = @arguments.dup(self) if @arguments.is_a?(Arguments)
|
73
|
+
@solutions = []
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Array<Porolog::Goal>] the calling chain for this goal.
|
77
|
+
def ancestors
|
78
|
+
ancestors = []
|
79
|
+
goal = self
|
80
|
+
|
81
|
+
while goal
|
82
|
+
ancestors << goal
|
83
|
+
goal = goal.calling_goal
|
84
|
+
end
|
85
|
+
|
86
|
+
ancestors.reverse
|
87
|
+
end
|
88
|
+
|
89
|
+
# A convenience method for testing/debugging.
|
90
|
+
# @return [String] the calling chain for this goal as a String.
|
91
|
+
def ancestry
|
92
|
+
indent = -1
|
93
|
+
ancestors.map do |goal|
|
94
|
+
indent += 1
|
95
|
+
"#{' ' * indent}#{goal.myid} -- #{goal.description} #{goal.variables.inspect}"
|
96
|
+
end.join("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
# A convenience method for testing/debugging.
|
100
|
+
# @return [String] the id of the goal.
|
101
|
+
def myid
|
102
|
+
"Goal#{(@@goals.index(self) || -1000) + 1}"
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [String] pretty representation.
|
106
|
+
def inspect
|
107
|
+
"#{myid} -- #{description}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# A convenience method for testing/debugging.
|
111
|
+
# @return [Array<Porolog::Goal>]
|
112
|
+
def self.goals
|
113
|
+
@@goals
|
114
|
+
end
|
115
|
+
|
116
|
+
# Delete the Goal
|
117
|
+
# @return [Boolean] whether the Goal has been deleted (memoized)
|
118
|
+
def delete!
|
119
|
+
@@goals.delete(self)
|
120
|
+
deleted?
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [Boolean] whether the Goal has been deleted (memoized)
|
124
|
+
def deleted?
|
125
|
+
@deleted ||= check_deleted
|
126
|
+
@deleted
|
127
|
+
end
|
128
|
+
|
129
|
+
# Determines whether the Goal has been deleted and removes its Variables and Values if it has.
|
130
|
+
# @return [Boolean] whether the Goal has been deleted
|
131
|
+
def check_deleted
|
132
|
+
return false if @@goals.include?(self)
|
133
|
+
|
134
|
+
@variables.delete_if do |_name,variable|
|
135
|
+
variable.remove
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
@values.delete_if do |_name,value|
|
140
|
+
value.remove
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
# Sets that the goal should no longer search any further rules for the current predicate
|
148
|
+
# once the current rule has finished being evaluated.
|
149
|
+
# This method implements the :CUT rule.
|
150
|
+
# That is, backtracking does not go back past the point of the cut.
|
151
|
+
# @return [Boolean] true.
|
152
|
+
def terminate!
|
153
|
+
@log << 'terminating'
|
154
|
+
@terminate = true
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Boolean] whether the goal has been cut.
|
158
|
+
def terminated?
|
159
|
+
@terminate
|
160
|
+
end
|
161
|
+
|
162
|
+
# Converts all embedded Symbols to Variables within this Goal.
|
163
|
+
# @param object [Object] the object to variablise.
|
164
|
+
# @return [Object] the variablised object.
|
165
|
+
def variablise(object)
|
166
|
+
case object
|
167
|
+
when Symbol,Variable
|
168
|
+
variable(object)
|
169
|
+
when Array
|
170
|
+
object.map{|element| variablise(element) }
|
171
|
+
when Arguments
|
172
|
+
object.dup(self)
|
173
|
+
when Tail
|
174
|
+
Tail.new variablise(object.value)
|
175
|
+
when Value
|
176
|
+
object
|
177
|
+
when NilClass
|
178
|
+
nil
|
179
|
+
else
|
180
|
+
if [UNKNOWN_TAIL, UNKNOWN_ARRAY].include?(object)
|
181
|
+
object
|
182
|
+
else
|
183
|
+
value(object)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
alias [] variablise
|
189
|
+
|
190
|
+
# @return [Hash{Symbol => Object}] the Variables and their current values of this Goal
|
191
|
+
def variables
|
192
|
+
@variables.keys.each_with_object({}){|variable,variable_list|
|
193
|
+
value = value_of(variable).value.value
|
194
|
+
if value.is_a?(Variable)
|
195
|
+
variable_list[variable] = nil
|
196
|
+
elsif value.is_a?(Array)
|
197
|
+
variable_list[variable] = value.clean
|
198
|
+
else
|
199
|
+
variable_list[variable] = value
|
200
|
+
end
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
204
|
+
# A convenience method for testing/debugging.
|
205
|
+
# @return [String] a tree representation of all the instantiations of this goal's variables.
|
206
|
+
def inspect_variables
|
207
|
+
@variables.values.map(&:inspect_with_instantiations).join("\n")
|
208
|
+
end
|
209
|
+
|
210
|
+
# A convenience method for testing/debugging.
|
211
|
+
# @return [Array<Object>] the values instantiated for this goal.
|
212
|
+
def values
|
213
|
+
@values.values.map(&:value)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Finds or tries to create a variable in the goal (as much as possible) otherwise passes the parameter back.
|
217
|
+
# @param name [Object] name of variable or variable
|
218
|
+
# @return [Porolog::Variable, Object] a variable of the goal or the original parameter
|
219
|
+
def variable(name)
|
220
|
+
return name unless name.is_a?(Symbol) || name.is_a?(Variable)
|
221
|
+
|
222
|
+
if name.is_a?(Variable)
|
223
|
+
variable = name
|
224
|
+
@variables[variable.name] = variable if variable.goal == self
|
225
|
+
return variable
|
226
|
+
end
|
227
|
+
|
228
|
+
@variables[name] ||= (name.is_a?(Variable) && name || Variable.new(name, self))
|
229
|
+
@variables[name]
|
230
|
+
end
|
231
|
+
|
232
|
+
# @param value [Object] the value that needs to be associated with this Goal.
|
233
|
+
# @return [Porolog::Value] a Value, ensuring it has been associated with this Goal so that it can be uninstantiated.
|
234
|
+
def value(value)
|
235
|
+
@values[value] ||= Value.new(value, self)
|
236
|
+
@values[value]
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the value of the indicated variable or value
|
240
|
+
# @param name [Symbol, Object] name of the variable or value
|
241
|
+
# @param index [Object] index is not yet used
|
242
|
+
# @param visited [Array] used to avoid infinite recursion
|
243
|
+
# @return [Object] the value
|
244
|
+
def value_of(name, index = nil, visited = [])
|
245
|
+
variable(name).value(visited)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns the values of an Object. For most objects, this will basically just be the object.
|
249
|
+
# For Arrays, an Array will be returned where the elements are the value of the elements.
|
250
|
+
# @param object [Object] the Object to find the values of.
|
251
|
+
# @return [Object,Array<Object>] the value(s) of the object.
|
252
|
+
def values_of(object)
|
253
|
+
case object
|
254
|
+
when Array
|
255
|
+
object.map{|element| values_of(element) }
|
256
|
+
when Variable,Symbol,Value
|
257
|
+
value_of(object)
|
258
|
+
when Tail
|
259
|
+
# TODO: This needs to splat outwards; in the meantime, it splats just the first.
|
260
|
+
value_of(object.value).first
|
261
|
+
else
|
262
|
+
object
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Finds all possible solutions to the Predicate for the specific Arguments.
|
267
|
+
# @param max_solutions [Integer] if provided, the search is stopped once the number of solutions has been found
|
268
|
+
# @return [Array<Hash{Symbol => Object}>]
|
269
|
+
def solve(max_solutions = nil)
|
270
|
+
return @solutions unless @arguments
|
271
|
+
|
272
|
+
predicate = @arguments.predicate
|
273
|
+
|
274
|
+
predicate&.satisfy(self) do |goal|
|
275
|
+
# TODO: Refactor to overrideable method (or another solution, say a lambda)
|
276
|
+
|
277
|
+
@solutions << variables
|
278
|
+
@log << "SOLUTION: #{variables}"
|
279
|
+
@log << goal.ancestry
|
280
|
+
@log << goal.inspect_variables
|
281
|
+
|
282
|
+
return @solutions if max_solutions && @solutions.size >= max_solutions
|
283
|
+
end
|
284
|
+
|
285
|
+
@solutions
|
286
|
+
end
|
287
|
+
|
288
|
+
# Solves the goal but not as the root goal for a query.
|
289
|
+
# That is, solves an intermediate goal.
|
290
|
+
# @param block [Proc] code to perform when the Goal is satisfied.
|
291
|
+
# @return [Boolean] whether the definition was satisfied.
|
292
|
+
def satisfy(&block)
|
293
|
+
return false unless @arguments
|
294
|
+
|
295
|
+
predicate = @arguments.predicate
|
296
|
+
|
297
|
+
satisfied = false
|
298
|
+
predicate&.satisfy(self) do |subgoal|
|
299
|
+
subgoal_satisfied = block.call(subgoal)
|
300
|
+
satisfied ||= subgoal_satisfied
|
301
|
+
end
|
302
|
+
satisfied
|
303
|
+
end
|
304
|
+
|
305
|
+
# Instantiates a Variable to another Variable or Value, for this Goal.
|
306
|
+
# @param name [Symbol,Porolog::Variable,Object] the name that references the variable to be instantiated.
|
307
|
+
# @param other [Object] the value that the variable is to be instantiated to.
|
308
|
+
# @param other_goal [Porolog::Goal,nil] the Goal of the other value.
|
309
|
+
# @return [Porolog::Instantiation] the instantiation.
|
310
|
+
def instantiate(name, other, other_goal = self)
|
311
|
+
variable = self.variable(name)
|
312
|
+
|
313
|
+
other = if other.type == :variable
|
314
|
+
other_goal.variable(other)
|
315
|
+
else
|
316
|
+
other_goal.value(other)
|
317
|
+
end
|
318
|
+
|
319
|
+
variable.instantiate(other)
|
320
|
+
end
|
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
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|