18446744073709551615
18446744073709551615

Reputation: 16832

"Prolog style" in Scala: mixing with procedural code?

Continuing What is "prolog style" in Scala?

I want to combine logical inference and procedural code, in Scala-3. Something like this:

// This is a mix of Prolog and Scala, and is not a real code.
// The idea is not only to find out that the goal is reachable,
// but to use the found solution

print_file(X:Filename) :- read(X,Representation), print(Representation).
read(X:Filename, CollectionOfLines[X]) :- { read_lines(X) }.
read(X:Filename, String[X]) :- { slurp_file(X) }.
print(X:String) :- { print(X) }.
print(X:CollectionOfLines) :- { X.foreach(println)}.

given Filename("foo.txt")
val howto = summon[print_file]
howto()

I expect such kind of program to print the specified file. But so far I do not know how to specify the procedural part, in Scala.

Upvotes: 2

Views: 128

Answers (1)

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

(This post contains code that is supposed to demonstrate some properties of the type system; it has no direct practical applications, please don't use it for anything; "It can be done" does not imply "you should do it")

In your question, it's not entirely clear what the formulas such as "CollectionOfLines[X]" and "String[X]" are supposed to mean. I took the liberty to warp it into something implementable:

/**
 * Claims that an `X` can be printed.
 */
trait Print[X]:
  def apply(x: X): Unit

/**
 * Claims that an instance of type `X` can
 * be read from a file with a statically
 * known `Name`.
 */
trait Read[Name <: String & Singleton, X]:
  def apply(): X

/**
 * Claims that an instance of type `Repr` can
 * be read from a statically known file with a given `Name`,
 * and then printed in `Repr`-specific way.
 */
trait PrintFile[Name, Repr]:
  def apply(): Unit

given splitLines: Conversion[String, List[String]] with
  def apply(s: String) = s.split("\n").toList

given printFile[Name <: String & Singleton, Repr]
  (using n: ValueOf[Name], rd: Read[Name, Repr], prnt: Print[Repr])
: PrintFile[Name, Repr] with
  def apply(): Unit =
    val content = rd()
    prnt(content)

given readThenConvert[Name <: String & Singleton, A, B]
  (using name: ValueOf[Name], readA: Read[Name, A], conv: Conversion[A, B])
: Read[Name, B] with
  def apply() = conv(readA())

inline given slurp[Name <: String & Singleton]
  (using n: ValueOf[Name])
: Read[Name, String] with
  def apply() = io.Source.fromFile(n.value).getLines.mkString("\n")

given printString: Print[String] with
  def apply(s: String) = println(s)

given printList[X](using printOne: Print[X]): Print[List[X]] with
  def apply(x: List[X]) = x.zipWithIndex.foreach((line, idx) => {
    print(s"${"%3d".format(idx)}| ")
    printOne(line)
  })

@main def printItself(): Unit =
  println("Print as monolithic string")
  summon[PrintFile["example.scala", String]]()
  println("=" * 80)
  println("Print as separate lines")
  summon[PrintFile["example.scala", List[String]]]()


When saved as example.scala, it will print its own code twice, once as one long multiline-string, and once as a list of numbered lines.

As already mentioned elsewhere, this particular use case seems highly atypical, and the code looks quite unidiomatic.

You should try to reserve this mechanism for making true and precise statements about types and type constructors, not for enforcing an order between a bunch of imperative statements.

In this example, instead of carefully making universally true statements, we're making a bunch of half-baked not well thought-out statements, giving them semi-random names, and then trying to abuse the nominal type system and to coerce the reality to the artificial constraints that we have imposed by naming things this or that. This is usually a sign of a bad design: everything feels loose and kind-of "flabby". The apply(): Unit in particular is clearly a red flag: there is not much that can be said about Unit that could also be encoded as types, so instead of relying on the type system, one has to revert to "stringly-typed" naming-discipline, and then hope that one has interpreted the names correctly.

Upvotes: 1

Related Questions