"Prolog style" in Scala: mixing with procedural code?
Asked Answered
F

1

2

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.

Factious answered 6/9, 2022 at 17:12 Comment(2)
As explained in the linked thread, the "prolog-style" subsystem is primarily intended for generating terms from types. It's not for printing anything or performing any side-effects. What is the term that you're attempting to summon here? What properties should it have? Again: I think you're trying to do something highly atypical here. I'd propose to get familiar with the more common patterns first, maybe go and implement a bunch of common type classes for your favorite data structures. This use case looks way too esoteric, maybe you should at least try to motivate it a little better.Pimento
It looks like now there are some questions about C++ syntax. Have the prolog-related problems already been solved?Pimento
P
1

(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.

Pimento answered 6/9, 2022 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.