Reputation: 16832
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
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