Skip to content

Commit fe5fe69

Browse files
committed
feat(solver): add utility extension method to check and cast the type of solutions
1 parent 32d2c15 commit fe5fe69

File tree

5 files changed

+113
-24
lines changed

5 files changed

+113
-24
lines changed

src/main/scala/io/github/kelvindev15/prolog/solver/Solver.scala

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import io.github.kelvindev15.prolog.solver.Solver.Solution
66
import io.github.kelvindev15.prolog.solver.Solver.Solution.{Halt, Yes}
77
import io.github.kelvindev15.prolog.solver.tuprolog.TuPrologClassicSolver
88

9+
import scala.reflect.ClassTag
10+
911
/** Instances of this trait solve [[PrologProgram]]s. */
1012
trait Solver:
1113

@@ -25,7 +27,8 @@ trait Solver:
2527
* @return
2628
* a lazy list of the program's [[Solution]]s.
2729
*/
28-
def lazySolve(program: PrologProgram): LazyList[Solution] = solve(program).to(LazyList)
30+
def lazySolve(program: PrologProgram): LazyList[Solution] =
31+
solve(program).to(LazyList)
2932

3033
/** Solves a program.
3134
*
@@ -34,12 +37,27 @@ trait Solver:
3437
* @return
3538
* a list of the program's [[Solution]]s.
3639
*/
37-
def solutionsOf(program: PrologProgram): Seq[Solution] = solve(program).to(Seq)
40+
def solutionsOf(program: PrologProgram): Seq[Solution] =
41+
solve(program).to(Seq)
3842

39-
def admitsSolutions(program: PrologProgram): Boolean =
43+
/** Returns true if the program admits at least one solution.
44+
*
45+
* @param program
46+
* the program to solve.
47+
*/
48+
def hasSolutionFor(program: PrologProgram): Boolean =
4049
val solutions = solve(program)
4150
solutions.hasNext && solutions.next().isInstanceOf[Solution.Yes]
4251

52+
/** Returns true if the provided goal admits at least one solution.
53+
*
54+
* @param goal
55+
* the program to satisfy.
56+
*/
57+
def hasSolutionFor(goal: Term): Boolean =
58+
val solutions = solve(PrologProgram.emptyTheory withGoal goal)
59+
solutions.hasNext && solutions.next().isInstanceOf[Solution.Yes]
60+
4361
object Solver:
4462
/** A mapping from [[Variable]]s to [[Term]]s */
4563
type Substitution = Map[Variable, Term]
@@ -64,6 +82,52 @@ object Solver:
6482
*/
6583
case Halt(exception: Exception)
6684

85+
extension (solution: Solution)
86+
def apply(variable: Variable): Option[Term] = solution match
87+
case y: Solution.Yes => Some(y.substitution(variable))
88+
case _ => None
89+
90+
/** Returns true if the solution is a [[Yes]]. */
91+
def isYes: Boolean = solution.isInstanceOf[Solution.Yes]
92+
93+
/** Returns true if the solution is a [[No]]. */
94+
def isNo: Boolean = solution.isInstanceOf[Solution.No]
95+
96+
/** Returns true if the solution is a [[Halt]]. */
97+
def isHalt: Boolean = solution.isInstanceOf[Solution.Halt]
98+
99+
/** Cast the solution to a [[Solution]] of type [[T]].
100+
*
101+
* @tparam T
102+
* the type of the expected solution
103+
*
104+
* @throws ClassCastException
105+
* if [[T]] is not the runtime type of the solution.
106+
*/
107+
def as[T <: Solution](using ClassTag[T]): Solution =
108+
solution.asInstanceOf[T]
109+
110+
/** Cast the solution to a [[Yes]] [[Solution]].
111+
*
112+
* @throws ClassCastException
113+
* if [[Yes]] is not the runtime type of the solution.
114+
*/
115+
def asYes: Solution = solution.as[Solution.Yes]
116+
117+
/** Cast the solution to a [[No]] [[Solution]].
118+
*
119+
* @throws ClassCastException
120+
* if [[No]] is not the runtime type of the solution.
121+
*/
122+
def asNo: Solution = solution.as[Solution.No]
123+
124+
/** Cast the solution to a [[Halt]] [[Solution]].
125+
*
126+
* @throws ClassCastException
127+
* if [[Halt]] is not the runtime type of the solution.
128+
*/
129+
def asHalt: Solution = solution.as[Solution.Halt]
130+
67131
/** Returns a Solver that leverages on the tuProlog engine. */
68132
def tuPrologSolver(): Solver = TuPrologClassicSolver()
69133

@@ -147,22 +211,26 @@ object Solver:
147211
): LazyList[Solution] =
148212
solver lazySolve (PrologProgram.emptyTheory withGoal query)
149213

150-
/** Returns true if the program admits at least one solution.
151-
*
152-
* @param solver the solver that should be used.
153-
* @param program the program to solve.
154-
*/
214+
/** Returns true if the program admits at least one solution. If not specified
215+
* the default solver that will be used is the [[TuPrologClassicSolver]].
216+
*
217+
* @param solver
218+
* the solver that should be used.
219+
* @param program
220+
* the program to solve.
221+
*/
155222
def hasSolutionForProgram(using solver: Solver = tuPrologSolver())(
156-
program: PrologProgram
157-
): Boolean =
158-
val solutions = solve(program)
159-
solutions.hasNext && solutions.next().isInstanceOf[Solution.Yes]
223+
program: PrologProgram
224+
): Boolean = solver hasSolutionFor program
160225

161-
/** Returns true if the program admits at least one solution.
162-
*
163-
* @param solver the solver that should be used.
164-
* @param goal the program to satisfy.
165-
*/
226+
/** Returns true if the provided goal admits at least one solution. If not
227+
* specified the default solver that will be used is the
228+
* [[TuPrologClassicSolver]].
229+
* @param solver
230+
* the solver that should be used.
231+
* @param goal
232+
* the program to satisfy.
233+
*/
166234
def hasSolutionForGoal(using solver: Solver = tuPrologSolver())(
167-
goal: Term
168-
): Boolean = hasSolutionForProgram(PrologProgram.emptyTheory withGoal goal)
235+
goal: Term
236+
): Boolean = solver hasSolutionFor goal

src/main/scala/io/github/kelvindev15/prolog/solver/tuprolog/TuPrologClassicSolver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private[tuprolog] object ConversionsUtils:
4848
_.asScala.map(identity)
4949

5050
/** A [[Solver]] that leverages on the tuProlog engine */
51-
class TuPrologClassicSolver extends Solver:
51+
private[solver] class TuPrologClassicSolver extends Solver:
5252
import ConversionsUtils.given
5353

5454
private def classicSolverOf(

src/test/scala/io/github/kelvindev15/prolog/solver/engine/TestPrologEngine.scala renamed to src/test/scala/io/github/kelvindev15/prolog/solver/engine/PrologEngineSpec.scala

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package io.github.kelvindev15.prolog.solver.engine
22

33
import io.github.kelvindev15.prolog.PrologProgram
4-
import io.github.kelvindev15.prolog.core.{PrologList, Term}
4+
import io.github.kelvindev15.prolog.core.{Constant, PrologList, Term}
55
import io.github.kelvindev15.prolog.dsl.{DeclarativeProlog, PrologDSL}
66
import io.github.kelvindev15.prolog.solver.Solver
77
import io.github.kelvindev15.prolog.solver.Solver.Solution
88
import io.github.kelvindev15.prolog.solver.engine.utils.EngineTestUtils
99
import org.scalatest.flatspec.AnyFlatSpec
10+
import org.scalatest.funsuite.AnyFunSuite
1011
import org.scalatest.matchers.should.Matchers
1112

1213
import scala.reflect
1314
import scala.reflect.ClassTag
1415

15-
class TestPrologEngine
16+
class PrologEngineSpec
1617
extends AnyFlatSpec
1718
with Matchers
1819
with EngineTestUtils
@@ -66,3 +67,24 @@ class TestPrologEngine
6667
" with at least one solution" in:
6768
assert(!(Solver hasSolutionForGoal member("notInList", list(1, 2, 4))))
6869
assert(Solver hasSolutionForGoal member("b", list("a", "b", "c", 1)))
70+
71+
class TestPrologEngine extends AnyFunSuite with Matchers with PrologDSL with DeclarativeProlog:
72+
test("Access to substitutions of Yes/No solutions"):
73+
val solutions = Solver query member(X, list(1, 2, 3))
74+
assert(solutions.hasNext)
75+
val first = solutions.next()
76+
assert(first.isYes)
77+
first.asYes(X) shouldBe Some(Constant(1))
78+
{ solutions.next(); solutions.next() }
79+
assert(solutions.hasNext)
80+
val last = solutions.next()
81+
assert(last.isNo)
82+
last.asNo(X) shouldBe None
83+
84+
test("Access to substitutions of Halt solutions"):
85+
val solutions = Solver query (X is Y)
86+
assert(solutions.hasNext)
87+
val theSolution = solutions.next()
88+
assert(theSolution.isHalt)
89+
theSolution.asHalt(X) shouldBe None
90+

src/test/scala/io/github/kelvindev15/prolog/solver/engine/TestBuiltins.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,4 @@ class TestBuiltins
138138

139139
test("Test operators"):
140140
expectOne[Solution.Yes]:
141-
Solver query (&&(X `=` 3, 2 < X))
141+
Solver query &&(X `=` 3, 2 < X)

src/test/scala/io/github/kelvindev15/prolog/solver/engine/utils/EngineTestUtils.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.kelvindev15.prolog.solver.engine.utils
22

3-
import io.github.kelvindev15.prolog.PrologProgram
43
import io.github.kelvindev15.prolog.core.{Struct, Term, Variable}
54
import io.github.kelvindev15.prolog.solver.Solver
65
import io.github.kelvindev15.prolog.solver.Solver.Solution.Yes

0 commit comments

Comments
 (0)