Part V — Logic programming and integration · Lesson 11

Java/Scala/Prolog integration and cross-platform Scala

~25 min of readingall Java and Scala examples compiled against tuProlog 4.0.3

In this lesson

  1. Why combine Java, Scala and Prolog?
  2. Which kinds of integration are there?
  3. How does Prolog call Java?
  4. How does Java call Prolog?
  5. How does a Java class become a Prolog library?
  6. How does Scala wrap a Prolog theory?
  7. How does Scala run outside the JVM?
  8. How does Scala talk to JavaScript and C?
  9. Check your understanding

The lesson in one paragraph. Prolog is good at search and symbolic reasoning, and Java and Scala are good at everything around it. tuProlog connects them in both directions. From Prolog, new_object/3, <-/2 and returns create Java objects and call their methods. From Java, an engine object solves goals given as strings or as Term objects, one solution at a time. A Java class can become a Prolog library, and a Scala wrapper can turn a theory into a function from goals to lazy streams of solutions. The second talk of the week moves Scala itself out of the JVM: Scala.js compiles to JavaScript, Scala Native to machine code, and facades bridge the two worlds.

1. Why combine Java, Scala and Prolog?

LanguageWhat it brings
Prologa declarative style; concise algorithms and data structures; built-in search and retry; rapid prototyping; (meta-)interpretation; closeness to abstract concepts
Javaimperative programming with OOP, easier for the typical programmer; more efficient than Prolog; a wide range of libraries (graphics, network, file system, XML, …); some declarativity through FP since Java 8
Scalagood Java integration; full FP; for-comprehensions that "somewhat" address search, though with less declarativity than LP

The proposed scenario: Java or Scala for the aspects "in the large" and the interaction with the operating system, Prolog for some data and behaviour of the core. Examples: modelling the mind of an agent in Prolog; writing the computer's strategy of a game in Prolog; storing data and knowledge in Prolog databases.

2. Which kinds of integration are there?

When language A uses language B, the integration can be:

tuProlog 4.0Prolog using JavaJava/Scala using Prolog
Library-oriented<-/2, new_object/3, returns/2 (Section 3)the Term hierarchy and the engine class (Section 4)
Encapsulation-orienteda Java class becomes a Prolog library (Section 5)a Prolog theory embedded in a Scala program (Section 6)

The slides' outline writes the first predicate as ->/2; the operator tuProlog defines, and the one the examples use, is <-.

3. How does Prolog call Java?

This Java program opens a window with the javax.swing library:

public class C {
  public static void main(String[] s) {
    javax.swing.JFrame f = new javax.swing.JFrame();
    f.setSize(100, 100);
    f.setVisible(true);
  }
}

The same in Prolog, run with ?- createframe('Prova', 400, 300).:

createframe(Name, X, Y) :-
    new_object('javax.swing.JFrame', [], Obj),
    Obj <- setTitle(Name),
    Obj <- setSize(X, Y),
    Obj <- setVisible(true).

A typical use is a small Prolog library over Java objects, here fast arrays of integers backed by java.util.Vector. The slides list vector_new(-V), vector_add(+V,+E), vector_addall(+V,+V2), vector_clear(+V), vector_contains(+V,+E), vector_lookup(+V,+Pos,-E), vector_isempty(+V), vector_size(+V,-S), vector_print(+V), and implement four:

vector_new(V) :- new_object('java.util.Vector', [], V).
vector_add(V, E) :- new_object('java.lang.Integer', [E], I), V <- add(I).
vector_size(V, S) :- V <- size returns S.
vector_lookup(V, P, E) :- V <- elementAt(P) returns E.

?- vector_new(X), vector_add(X,1), vector_add(X,2), vector_lookup(X,0,N).   % X = '$obj_0', N = 1
?- vector_new(X), vector_add(X,1), vector_add(X,2), vector_size(X,S).       % X = '$obj_0', S = 2
Beyond the slides

From Java, tuProlog 4.0.3 parses new Theory(String) without the operators of its libraries, so a theory that uses <- or returns fails with a syntax error. Parse it with the engine's operators instead: engine.setTheory(Theory.parseLazilyWithOperators(text, engine.getOperatorManager())). Goals passed to solve are not affected. The JFrame example needs a graphical display.

4. How does Java call Prolog?

The tuProlog library (package alice.tuprolog) offers classes for an engine, a term and a solution, and methods to solve goals, ask for further solutions and navigate terms:

The slides' five examples (package u12 and the throws Exception clauses as in the slides), with the output they print:

// libraryDependencies += "it.unibo.alice.tuprolog" % "tuprolog" % "3.3.0"
import alice.tuprolog.*;

public class Test1 {                       // direct goal solution
  public static void main(String[] args) throws Exception {
    final Prolog engine = new Prolog();
    final SolveInfo info = engine.solve("append([1],[2,3],X).");
    System.out.println(info.getSolution());
    // append([1],[2,3],[1,2,3])
  }
}
public class Test2 {                       // getting all solutions
  public static void main(String[] args) throws Exception {
    final Prolog engine = new Prolog();
    SolveInfo info = engine.solve("append(X,Y,[1,2,3]).");
    while (info.isSuccess()) {
      System.out.print("solution: " + info.getSolution() + " - ");
      System.out.println("bindings: X/" + info.getTerm("X") + " Y/" + info.getTerm("Y"));
      if (engine.hasOpenAlternatives()) {
        info = engine.solveNext();
      } else {
        break;
      }
    }
    // solution: append([],[1,2,3],[1,2,3]) - bindings: X/[] Y/[1,2,3]
    // solution: append([1],[2,3],[1,2,3]) - bindings: X/[1] Y/[2,3]
    // solution: append([1,2],[3],[1,2,3]) - bindings: X/[1,2] Y/[3]
    // solution: append([1,2,3],[],[1,2,3]) - bindings: X/[1,2,3] Y/[]
  }
}
public class Test3 {                       // creating and using terms
  public static void main(String[] args) throws Exception {
    final Prolog engine = new Prolog();
    final Term list1 = engine.toTerm("[1]");                              // [1]
    final Term list2 = new Struct(new Term[]{new Int(2), new Int(3)});    // [2,3]
    final Term app = new Struct("append", list1, list2, new Var("X"));    // append([1],[2,3],X)
    final SolveInfo info = engine.solve(app);
    System.out.println(info.getSolution());
    // append([1],[2,3],[1,2,3])
  }
}
public class Test4 {                       // reading terms
  public static void main(String[] args) throws Exception {
    final Prolog engine = new Prolog();
    Term t = new Struct();                          // []
    for (int i = 10; i >= 0; i--) {
      t = new Struct(new Int(i), t);                // t will be [0,1,2,...,9,10]
    }
    final Term app = new Struct("append", t, t, new Var("X"));
    final SolveInfo info = engine.solve(app);
    final Struct t2 = (Struct) info.getTerm("X");
    for (java.util.Iterator<? extends Term> i = t2.listIterator(); i.hasNext();) {
      System.out.print(" " + i.next());
    }
    // 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10
  }
}
public class Test5 {                       // setting a theory
  public static void main(String[] args) throws Exception {
    final Prolog engine = new Prolog();
    final Theory t = new Theory(
        "search(E,[E|_]). " +               // a space is needed here!
        "search(E,[_|L]) :- search(E,L).");
    // engine.setTheory(new Theory(new FileInputStream("file.pl")));
    engine.setTheory(t);
    final SolveInfo info = engine.solve("search(1,[1,2,3]).");
    System.out.println(" " + info.getSolution());
    // search(1,[1,2,3])
  }
}

The space matters because a clause ends with a period followed by white space; without it tuProlog rejects the theory. Test4's iterator is typed here as Iterator<? extends Term>; the slide uses a raw Iterator, which also compiles.

Beyond the slides

The slides' comment names the tuProlog 3.3.0 dependency; the course jar is 4.0.3, where all five examples compile and print the lines above. In 4.0.3 the constructors used in Test3–Test5 are deprecated, with static factories in their place: Struct.of("append", …), Struct.list(…), Struct.emptyList(), Int.of(2), Var.of("X"), Theory.parse(…).

When is it worth it? Whenever something is better implemented in Prolog. The slides' exercise: permutations are a one-line idea in Prolog, perm(+L, -L2); write a Java method that takes a LinkedList and returns an Iterator<LinkedList>, whose next() calls solveNext() and builds the Java list from the resulting term.

5. How does a Java class become a Prolog library?

A Prolog library is basically a Prolog database that defines many predicates. It is loaded from a theory with the goal load_library/1, or in the IDE with the library manager. The IDE loads the built-in ones automatically: BasicLibrary (standard predicates such as append), ISOLibrary (the predicates of the ISO standard), IOLibrary (input/output, e.g. tell, told).

Writing one in Java is an elegant, encapsulation-oriented approach. Write a class in which each method defines a predicate, compile it (say as lib.mylibrary) and load it with load_library('lib.mylibrary'). The method's name and signature decide what it defines:

// run with: java -cp <path-to-libraries>:2p.jar alice.tuprologx.ide.GUILauncher
// be sure the library is compiled with Java 8
import alice.tuprolog.*;
import alice.tuprolog.Number;   // not in the slides: without it, Number is ambiguous with java.lang.Number

public class TestLibrary extends Library {

  private int termAsInt(Term t) {
    return ((Number) t).intValue();
  }

  // try with: X is sum(10,20).
  public Term sum_2(Term arg0, Term arg1) {
    return new Int(termAsInt(arg0) + termAsInt(arg1));
  }

  // try with: X is minus(20,10).
  public Term minus_2(Term arg0, Term arg1) {
    return new Int(termAsInt(arg0) - termAsInt(arg1));
  }

  // try with: sum(Y,8,21).
  public boolean sum_3(Term arg0, Term arg1, Term arg) {
    try {
      if (arg instanceof Var) {
        return arg.unify(this.getEngine(), sum_2(arg0, arg1));
      }
      if (arg0 instanceof Var) {
        return arg0.unify(this.getEngine(), minus_2(arg, arg1));
      }
      return arg1.unify(this.getEngine(), minus_2(arg, arg0));
    } catch (Exception e) {
      return false;
    }
  }
}
?- X is sum(10,20).    % X = 30
?- X is minus(20,10).  % X = 10
?- sum(Y,8,21).        % Y = 13
?- sum(10,8,Z).        % Z = 18
?- sum(10,Y,21).       % Y = 11
?- sum(10,8,19).       % no

sum_3 is relational in the Java way: it looks at which argument is a variable and asks the engine to unify it with the value computed from the other two. arg.unify(getEngine(), term) returns whether the unification succeeded, and that is the success of the predicate. The last query is the "check" mode: Z is not a variable, so the method unifies 8 with 19 − 10 = 9, which fails.

Beyond the slides

With import alice.tuprolog.* alone the class does not compile: both alice.tuprolog and java.lang have a Number, hence the explicit import. The class must be public, and load_library/1 finds it by name on the classpath, as the java -cp comment says. From Java you can skip the classpath and load an instance: engine.loadLibrary(new TestLibrary()). That is how the results above were obtained.

6. How does Scala wrap a Prolog theory?

The encapsulation-oriented approach on the Scala side: a theory becomes a Scala function from goals to lazy streams of solutions.

// with dependency: "it.unibo.alice.tuprolog" % "tuprolog" % "3.3.0"
import alice.tuprolog.*

object Scala2P:
  given Conversion[String, Term] = Term.createTerm(_)
  given Conversion[Seq[_], Term] = _.mkString("[", ",", "]")

  def extractTerm(t: Term, i: Integer): Term =
    t.asInstanceOf[Struct].getArg(i).getTerm

  def mkPrologEngine(clauses: String*): Term => LazyList[Term] =
    val engine = Prolog()
    engine.setTheory(Theory(clauses mkString " "))
    goal => new Iterable[Term]{
      override def iterator = new Iterator[Term]{
        var solution = engine.solve(goal);
        override def hasNext =
          solution.isSuccess || solution.hasOpenAlternatives
        override def next() =
          try { solution.getSolution } finally { solution = engine.solveNext }
      }
    }.to(LazyList)
object TryScala2P extends App:
  import Scala2P.{*, given}

  val engine: Term => LazyList[Term] = mkPrologEngine("""
    member([H|T],H,T).
    member([H|T],E,[H|T2]):- member(T,E,T2).
    permutation([],[]).
    permutation(L,[H|TP]) :- member(L,H,T), permutation(T,TP).
  """)

  engine("permutation([1,2,3],L)") foreach (println(_))
  // permutation([1,2,3],[1,2,3]) ... permutation([1,2,3],[3,2,1])   (six lines)

  val input = Struct("permutation", (1 to 20), Var())
  engine(input) map (extractTerm(_, 1)) foreach (println(_))
  // [1,2,3,4,...,20]  [1,2,...,18,20,19]  [1,2,...,17,19,18,20]  ... (20! of them, computed on demand)

How the pieces fit:

A bug in the wrapper, and a fix

next() calls solveNext even when the engine has no open alternatives. That is harmless for permutation, whose search ends with a failure, but a goal whose last answer is deterministic throws: engine("append([1],[2],X)").toList raises NoMoreSolutionException. Asking the engine first fixes it:

    goal =>
      LazyList.unfold(Option(engine.solve(goal)).filter(_.isSuccess)): info =>
        info.map(i => (i.getSolution,
          Option.when(engine.hasOpenAlternatives)(engine.solveNext).filter(_.isSuccess)))

// engine("append([1],[2],X)").toList   ==  List(append([1],[2],[1,2]))
// engine("member([1,2],E,R)").toList   ==  List(member([1,2],1,[2]), member([1,2],2,[1]))

Both versions share one engine among all goals, so streams must not be consumed in an interleaved way. Read the first answer of member([1,2,3],E,R), then one of member([a,b],E,R), then go back to the first stream: with either version it gives its second answer and then ends early, without member([1,2,3],3,[1,2]). If the second stream is only created and not read, the version above even returns member([a,b],b,[a]) as the first stream's third answer.

A preview: logic programming in plain Scala

The same search can be written without an engine. A fully relational predicate becomes a function returning the LazyList of all its solutions, and the "mode" is chosen afterwards by filtering the tuples:

// lookup([H|T], H, zero, T).
// lookup([H|T], E, s(N), [H|T2]) :- lookup(T, E, N, T2).

def lookup[A](list: List[A]): LazyList[(A, Int, List[A])] = list match
  case Nil => LazyList()
  case h :: t =>
    (h, 0, t) #::
      (for (e, n, t2) <- lookup(t) yield (e, n + 1, h :: t2))

println(lookup(List(10, 20, 30, 20)).toList)
// List((10,0,List(20, 30, 20)), (20,1,List(10, 30, 20)), (30,2,List(10, 20, 20)), (20,3,List(10, 20, 30)))
println(lookup(List(10, 20, 30, 20)).collect { case (20, n, _) => n }.toList)   // List(1, 3)
println(lookup(List(10, 20, 30, 40)).collect { case (e, 3, _) => e }.toList)    // List(40)

The slide's output comments show positions as zero, s(zero), … as in the Prolog version; the Scala code uses Int, and prints 0, 1, 3.

From the lab: a planner in pure Prolog (Lab 12, part 3), with one possible solution

A robot lives in a 4×4 grid. Positions are s(X,Y); init(s(0,0)) and goal(s(3,3)) give the start and the goal; the commands are the atoms up, down, left, right; move(D, S, S2) moves the robot when the target cell is inside the world. The exercise: write plan(+MaxN, -Trace), a list of at most MaxN commands from the start to the goal. Then extend the world (pits that destroy the robot, jumps between cells, a one-time jump over two cells) and attach a GUI, either calling Java from Prolog or wrapping the theory in Scala and drawing the plan there.

init(s(0,0)).                                  % initial condition
goal(s(3,3)).                                  % goal
move(up, s(X,Y), s(X2,Y)) :- X > 0, X2 is X-1.
move(down, s(X,Y), s(X2,Y)) :- X < 3, X2 is X+1.
move(left, s(X,Y), s(X,Y2)) :- Y > 0, Y2 is Y-1.
move(right, s(X,Y), s(X,Y2)) :- Y < 3, Y2 is Y+1.

% one possible plan/2 (not from the slides)
plan(MaxN, Trace) :- init(S), plan(S, MaxN, Trace).
plan(S, _, []) :- goal(S).
plan(S, N, [D|T]) :- N > 0, move(D, S, S2), N2 is N-1, plan(S2, N2, T).

?- plan(6, T).    % T = [down,down,down,right,right,right] ; T = [down,down,right,down,right,right] ; ...  (20 plans)
?- plan(5, T).    % no

The slide's facts and rules lack their final periods; they are added here. Parts 1 and 2 of the lab (meta-interpretation and generic predicates) are in Lesson 10.

7. How does Scala run outside the JVM?

(Guest talk by G. Aguzzi.) Cross-platform software, also called platform-agnostic or multi-platform, runs on several target platforms (operating systems, architectures, runtimes) from one codebase. The motivations: lower development costs, no duplicated code, and business logic that stays in sync across platforms. The classical model is the JVM's "write once, run everywhere" with portable bytecode. The modern shift is target compilation: transpilation from one high-level language to another (Scala, Kotlin or Dart to JavaScript), or ahead-of-time compilation to machine code through toolchains such as LLVM (Scala Native, Rust, Go). Browser JIT engines and WebAssembly (Wasm) runtimes are the new portable environments, while UI SDKs stay platform-specific and share a core written in one language.

From the slides: frameworks and languages for cross-platform development

For Scala, the benefits are a shared codebase (no duplication, no error propagation), full-stack development with one language for backend and client, and access to platform libraries that were not written for Scala (TensorFlow from Scala). The use cases: cross-platform libraries (e.g. Cats), web applications (Scala.js), cloud lambda functions or containers (native), code shared by Android and iOS apps. Compiling target code to Scala is usually not considered.

Configuring and compiling

scalac does not produce JavaScript or native executables by itself: SBT plugins do it. Plugins go in project/plugins.sbt, the projects in build.sbt:

// project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0")
// for native
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10")

// build.sbt
import sbt.Keys.scalaVersion
ThisBuild / scalaVersion := "3.3.5"
name := "scala-js-native"
// multi-project configuration: each val is a subproject with that name
val js = project.settings(
  scalaJSUseMainModuleInitializer := true
).enablePlugins(ScalaJSPlugin)
val native = project
  .settings(
    nativeConfig ~= { _.withBaseName("native-out") }
  )
  .enablePlugins(ScalaNativePlugin)

How it works (from S. Doeraene's thesis, Cross-Platform Language Design, EPFL 2018): a compiler plugin turns plain Scala into an intermediate representation (IR) that contains the platform-dependent aspects; from the IR the toolchain optimises, links and manages dependencies. Scala 3 supports both targets out of the box, and givens, extension methods, union types and enums compile to JS and native code without performance penalties. The Metals IDE switches between the JVM, JS and native environments, and TASTy (typed abstract syntax trees) improves the binary compatibility of libraries across targets. Since Scala.js 1.17 an experimental WebAssembly target uses the WasmGC and exception-handling proposals: faster compute-heavy code and less JIT warm-up in browsers and Node.js.

// build.sbt: enabling Wasm
scalaJSLinkerConfig := {
  scalaJSLinkerConfig.value
    .withExperimentalUseWebAssembly(true)
    .withModuleKind(ModuleKind.ESModule)
}

Limits, and how mature each target is

A pure cross-platform project cannot use the JVM ecosystem (threads, file systems) unless a cross-platform facade or a reimplemented library exists. Output bundles include parts of the Scala runtime and library: a Scala.js bundle easily reaches about 1 MB before tree-shaking and minification.

Scala.js: production-readyScala Native: promising, still maturing
Stable since 2014, near-perfect compatibility with standard Scala. Garbage collection is left to the host browser (V8, SpiderMonkey). Rich pure-Scala reactive frameworks (Laminar, Tyrian).Standalone executables, instant startup, small memory footprint. But LLVM linking is slow, the standard library has gaps, the GC (Boehm, Immix) is less mature than the JVM's or the browsers', and no tool like ScalablyTyped exists for C/C++ headers, so bindings need verbose manual glue code.

8. How does Scala talk to JavaScript and C?

Scala code needs type declarations that bridge Scala types to JS globals or C headers. In Scala.js they are facades: ordinary Scala objects or classes annotated to tell the compiler that they stand for JavaScript APIs. In Scala Native they are bindings to C libraries, with @extern and low-level pointers (Ptr[Byte]).

// Scala.js facade (browser alert)
import scala.scalajs.js
import scala.scalajs.js.annotation.*

@js.native
@JSGlobal
object window extends js.Object {
  def alert(msg: String): Unit = js.native
}

// Scala Native C binding (math)
import scala.scalanative.unsafe.*

@link("m")
@extern
object libm {
  def sqrt(d: Double): Double = extern
}

The slides omit the imports, added here; with them the facade compiles with Scala.js 1.22 (the binding was not compiled: no LLVM toolchain was available). Scala.js interoperability rests on three pillars:

Writing facades by hand for large libraries (React, Three.js, Lodash) is practically impossible. But TypeScript's DefinitelyTyped repository has .d.ts type definitions for almost every NPM package, and ScalablyTyped, a compiler and SBT plugin, turns them into type-safe, idiomatic Scala.js facades. TypeScript modules become Scala packages, and union types, structural types and callbacks become Scala 3 union types, generics and functions, checked at compile time. The whole NPM ecosystem becomes usable with type safety and IDE completion. A clean wrapper then hides from its clients the interop annotations, the collection conversions and low-level details such as calling dispose() on tensors:

import scala.scalajs.js
import customtypings.tensorflowTfjs.mod as tf

val data = js.Array(1.0, 2.0, 3.0, 4.0)
val tensor = tf.tensor1d(data)
val squared = tensor.square()
tensor.dispose()
From the slides: ScalablyTyped configuration, build styles and flavours
lazy val scalablytyped = (project in file("scalablytyped"))
  .enablePlugins(ScalaJSBundlerPlugin, ScalablyTypedConverterGenSourcePlugin)
  .settings(name := "scala-js-facade-scalablytyped",
    scalaJSUseMainModuleInitializer := true,
    Compile / npmDependencies += "@tensorflow/tfjs" -> "4.22.0",
    stOutputPackage := "customtypings")

Build styles: the managed bundler (ScalablyTypedConverterPlugin), centralised in SBT, reads npmDependencies and compiles facades on the fly; the external NPM style (ScalablyTypedConverterExternalNpmPlugin) uses your package.json and node_modules, for tools like Vite and a faster build loop. Output flavours (stFlavour): Flavour.Normal (default, raw JS objects, e.g. Three.js, TensorFlow.js), Flavour.Slinky (for the Slinky React wrapper), Flavour.ScalajsReact (for Japgolly's scalajs-react, functional and Cats-based).

Raw facades are rarely needed for the UI: Laminar is a pure-Scala reactive UI library based on observables, which targets the DOM directly without JS dependencies; Tyrian, from the Typelevel ecosystem, follows the Elm architecture (model-view-update) and integrates with Cats Effect.

One project, three targets

The sbt-crossproject plugin sets up multi-platform projects with a standard layout. CrossType.Pure is for pure cross-platform projects such as libraries, with all the code under src/. CrossType.Full is for projects with platform-specific code: shared holds the common code (data structures, interfaces, …), and js, jvm and native the platform code (library usage, GUIs, …). Dependencies use %%% so that each platform gets its own artifact.

// project/plugins.sbt
val crossOrg = "org.portable-scala"
val crossNative = "sbt-scala-native-crossproject"
val crossJs = "sbt-scalajs-crossproject"
addSbtPlugin(crossOrg % crossJs % "1.3.2")
addSbtPlugin(crossOrg % crossNative % "1.3.2")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10")

// build.sbt
crossProject(JSPlatform, NativePlatform, JVMPlatform)
  .crossType(CrossType.Full)            // or CrossType.Pure
  // common settings, use %%% for cross libraries
  .settings(
    libraryDependencies ++= Seq(
      "org.scalatest" %%% "scalatest" % "3.2.19"
    )
  )
  .jsSettings()
  .nativeSettings()
  .jvmSettings()                        // standard "scala" world

The architecture that fits is clean (hexagonal) architecture, ports and adapters. The pure core, in the shared project, holds domain models, business logic, validation rules and pure functions, and uses only cross-platform libraries (e.g. Cats, Circe). Platform adapters implement the ports per target: database engines and server frameworks on the JVM (JDBC, Doobie), DOM and browser storage in JS (Laminar, Tyrian), direct memory, POSIX files and C interop in native code. Generic traits are defined in the shared module and implemented on each platform; SBT handles cross-linking and packaging.

Most major Scala libraries are published for all three targets at once, so business logic, data validation and test suites can be shared: Cats, Cats Effect and FS2 (functional programming, async runtime, streaming); Http4s, Tapir and Smithy4s (HTTP and shared API definitions); Circe and uPickle (JSON); MUnit and Weaver (testing, identical on every target).

From the slides: the example projects

A TensorFlow.js facade (a memory-safe, pure-Scala API over Google's library); Winning Four, a game with shared core logic and platform-specific GUIs (web, Android); a full-stack Todo service that shares validation and schemas between a JVM backend and a Scala.js frontend; Scala Boids, a flocking simulation with one codebase for several targets.

Check your understanding

What are the four kinds of language integration, and which does tuProlog use?

Library-oriented, language-oriented, encapsulation-oriented and DSL-embedding. tuProlog uses the first (Prolog calling Java with new_object/<-/returns; Java calling Prolog through the engine and Term API) and the third (Java classes as Prolog libraries; a theory embedded in Scala).

How do you create a Java object from Prolog and read the result of a method?

new_object('java.util.Vector', [], V) binds V to a reference term; V <- add(I) calls a method whose result is not needed; V <- size returns S unifies S with the result.

How do you get all the solutions of a goal from Java?

engine.solve(goal) returns a SolveInfo; while it is a success, read getSolution() or getTerm("X"), and if engine.hasOpenAlternatives() call engine.solveNext(), otherwise stop.

What is the Term hierarchy, and how is append([1],[2,3],X) built without parsing?

Term has subclasses Var, Number (Int, Long, Float, Double) and Struct (compound terms and lists). new Struct("append", list1, new Struct(new Term[]{new Int(2), new Int(3)}), new Var("X")), with list1 built the same way or parsed.

In a tuProlog library class, what is the difference between public boolean sum_3(…) and public Term sum_2(…)?

The first defines the predicate sum/3, which succeeds when the method returns true, binding arguments with unify. The second defines an evaluable function sum/2, used as in X is sum(10,20).

What does mkPrologEngine return, and why a LazyList?

A function Term => LazyList[Term]: for each goal, the stream of its solutions, backed by solve and solveNext. Being lazy, the stream computes solutions only when consumed, so goals with huge or infinite numbers of solutions, like the permutations of 1 to 20, can be used.

What do Scala.js and Scala Native produce, and how?

JavaScript (or experimentally WebAssembly) and native executables through LLVM. SBT plugins make the compiler emit an intermediate representation, which the toolchain optimises and links for the target.

What is a Scala.js facade, and what does ScalablyTyped add?

A Scala type annotated with @js.native (and @JSGlobal or @JSImport) that describes an existing JS API: no code is generated, calls are emitted correctly. ScalablyTyped generates such facades automatically from TypeScript .d.ts definitions.

How do you organise a project for JVM, JS and native targets?

With sbt-crossproject: crossProject(JSPlatform, NativePlatform, JVMPlatform) with CrossType.Full, a shared module for the pure core and one module per platform for the adapters, following ports and adapters; cross-published libraries are added with %%%.

Where this lesson comes from

Deck 12b-JavaScalaProlog (all 38 slides) · Deck 12c-scala-cross-platform (all 24 slides, guest talk by G. Aguzzi) · Lab 12, part 3 (the planner); parts 1 and 2 are in Lesson 10 · 12-code.txt contains only the metaprogramming code of Lesson 10. Checks: Test1–Test5 and TestLibrary run with Java 21 against tuProlog 4.0.3; the vector predicates and the planner on tuProlog 4.0.3; Scala2P, its fix and lookup with Scala 3.3.6; the Scala.js facade compiled with Scala.js 1.22 through scala-cli. Not compiled: the SBT build files, the ScalablyTyped example and the Scala Native binding (no SBT and no LLVM toolchain available).