Each language in the trio brings distinct strengths:
| Language | Strengths | Role in integration |
|---|---|---|
| Prolog | Declarative style, concise algorithms, intrinsic search and backtracking, rapid prototyping, meta-programming facilities, natural for symbolic AI | Core logic, reasoning, search algorithms |
| Java | Imperative + OOP, massive library ecosystem (graphics, networking, file I/O, XML), efficient, widely understood | Infrastructure, I/O, GUI, libraries |
| Scala | Full Java interop, full FP support, for-comprehension for search, modern OOP + FP integration | Glue layer, FP encapsulation of Prolog |
Use Java/Scala for aspects "in the large" and interactions with the operating system. Use Prolog for core data and behaviour that benefits from search and declarative specification — like modelling an agent's mind, a game's computer strategy, or storing and querying knowledge.
Example application areas: an agent whose reasoning engine is written in Prolog; a game where the AI opponent's strategy is specified declaratively in Prolog; using Prolog databases for storing knowledge and running inference rules.
The course identifies four integration strategies, each with different trade-offs:
| Approach | Description | Example |
|---|---|---|
| Library-oriented | A library for language A allows exploiting language B. Handy but the glue code can be complicated. | tuProlog's Term hierarchy, Prolog engine API |
| Language-oriented | Language A is extended with abstractions from B. Often too complicated. | Hypothetical "Java+Prolog" combined syntax |
| Encapsulation-oriented | Code in B is encapsulated into A's abstractions. Nice trade-off. | Java class becomes Prolog library; Scala wraps Prolog theory |
| DSL-embedding (modern) | B's features are provided as an internal DSL in A (not used in tuProlog). | Scala DSL for logic programming |
In tuProlog 4.0, the following technologies are available for each direction:
->/2, new_object/3, returns/2tuProlog allows Prolog code to create and manipulate Java objects directly through three key predicates.
Creates a Java object from Prolog:
new_object(+ClassName, +ArgumentList, -ObjRef)
Calls the Java constructor of ClassName, passing ArgumentList, and unifying ObjRef with the resulting object reference (an opaque term like '$obj2').
Calls a method on a Java object (no return value expected):
ObjRef <- MethName(Args)
Calls a method and captures the return value:
ObjRef <- MethName(Args) returns ObjRef2
A library-like Prolog module wrapping java.util.Vector:
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.
% Usage:
% ?- vector_new(X), vector_add(X,1), vector_add(X,2), vector_lookup(X, 0, N).
The Java interoperability predicates effectively turn Prolog into a scripting language for the JVM, giving it access to the entire Java standard library and any other JVM-hosted library.
The tuProlog Java library provides a full API for embedding a Prolog engine in Java applications. The main classes are in the alice.tuprolog package.
| Class | Purpose |
|---|---|
Prolog | The Prolog virtual machine (engine) |
Theory | A Prolog theory (database), as text or list of clauses |
SolveInfo | The outcome of solving a goal |
Term | The root of the term hierarchy: Var, Number (Int, Double, etc.), Struct |
import alice.tuprolog.*;
public class Test1 {
public static void main(String[] args) throws Exception {
Prolog engine = new Prolog();
SolveInfo info = engine.solve("append([1],[2,3],X).");
System.out.println(info.getSolution());
// "append([1],[2,3],[1,2,3])"
}
}
public class Test2 {
public static void main(String[] args) throws Exception {
Prolog engine = new Prolog();
SolveInfo info = engine.solve("append(X,Y,[1,2,3]).");
while (info.isSuccess()) {
System.out.println("solution: " + info.getSolution()
+ " - bindings: X/" + info.getTerm("X")
+ " Y/" + info.getTerm("Y"));
if (engine.hasOpenAlternatives()) {
info = engine.solveNext();
} else { break; }
}
}
}
public class Test3 {
public static void main(String[] args) throws Exception {
Prolog engine = new Prolog();
// Parse a term from string
Term list1 = engine.toTerm("[1]");
// Build terms programmatically
Term list2 = new Struct(new Term[]{new Int(2), new Int(3)});
// Build compound term
Term app = new Struct("append", list1, list2, new Var("X"));
SolveInfo info = engine.solve(app);
System.out.println(info.getSolution());
// "append([1],[2,3],[1,2,3])"
}
}
public class Test4 {
public static void main(String[] args) throws Exception {
Prolog engine = new Prolog();
Term t = new Struct();
for (int i = 10; i >= 0; i--) {
t = new Struct(new Int(i), t);
}
Term app = new Struct("append", t, t, new Var("X"));
SolveInfo info = engine.solve(app);
Struct t2 = (Struct) info.getTerm("X");
for (java.util.Iterator i = t2.listIterator(); i.hasNext();) {
System.out.print(" " + i.next());
}
// 0 1 2 ... 10 0 1 2 ... 10
}
}
public class Test5 {
public static void main(String[] args) throws Exception {
Prolog engine = new Prolog();
Theory t = new Theory(
"search(E,[E|_]). " +
"search(E,[_|L]) :- search(E,L)."
);
engine.setTheory(t);
SolveInfo info = engine.solve("search(1,[1,2,3]).");
System.out.println(" " + info.getSolution());
}
}
You should be able to write Java code that creates a Prolog engine, sets a theory, solves a goal, and iterates through multiple solutions. Be prepared to explain the Term hierarchy and how to build and inspect terms programmatically.
This is the encapsulation-oriented approach for Prolog using Java: a Java class that implements a Prolog library, where each Java method defines a Prolog predicate.
Librarypublic boolean <name>_<arity>(T1 arg1, ..., Tn argN)public Term <name>_<arity>(T1 arg1, ..., Tn argN)Term, Var, Number, etc.load_library('lib.mylibrary').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; }
}
}
The sum_3 method demonstrates a predicate that works in multiple modes: if the result is a variable, it computes the sum; if one input is a variable, it computes the difference. This is the Java side implementing Prolog-style full relationality.
The most elegant integration uses Scala's functional abstractions to wrap Prolog theories, treating solutions as a lazy stream. This is the encapsulation-oriented approach applied to Scala.
Scala provides implicit conversions and an engine factory:
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).
""")
// Solutions as a lazy stream
engine("permutation([1,2,3], L)") foreach (println(_))
// permutation([1,2,3],[1,2,3]) ... permutation([1,2,3],[3,2,1])
// Using structured terms
val input = Struct("permutation", (1 to 20), Var())
engine(input) map (extractTerm(_, 1)) foreach (println(_))
The LazyList wrapping makes Prolog's multiple solutions behave like a monadic stream — you can map, filter, and for-comprehend over them, bridging the LP and FP paradigms.
The course also shows a pure-FP encoding of Prolog-like search in Scala, demonstrating how the same patterns can be expressed without a Prolog engine:
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)), ...)
// Find positions of 20
println(lookup(List(10, 20, 30, 20))
.collect { case (20, n, _) => n }.toList)
// List(1, 3)
Scala is not just a JVM language. It compiles to JavaScript (Scala.js) and native machine code (Scala Native), enabling write once, run on three platforms.
| Target | Output | Use case | Maturity |
|---|---|---|---|
| JVM | Bytecode | Backend services, enterprise, Android | Production-ready |
| Scala.js | JavaScript (or WebAssembly) | Web frontends, Node.js, browser libraries | Stable since 2014 |
| Scala Native | Native executable (LLVM) | CLI tools, embedded, cloud functions | Maturing |
// project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0")
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"
val js = project.settings(
scalaJSUseMainModuleInitializer := true
).enablePlugins(ScalaJSPlugin)
val native = project.settings(
nativeConfig ~= { _.withBaseName("native-out") }
).enablePlugins(ScalaNativePlugin)
// Using sbt-crossproject for multi-platform builds
lazy val shared = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Full) // or CrossType.Pure
.settings(libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % "3.2.19"
))
.jsSettings(/* JS-specific */)
.jvmSettings(/* JVM-specific */)
.nativeSettings(/* Native-specific */)
The compiler takes plain Scala code and, using a compiler plugin, produces an intermediate representation (IR). The IR is then optimised, linked, and compiled to the target platform. Scala 3 has native, out-of-the-box support for both JavaScript and Native targets with advanced features like givens, extension methods, and enums compiling seamlessly.
The recommended pattern is Ports & Adapters (Hexagonal Architecture):
shared/): domain models, business logic, pure functions using only cross-platform libraries (Cats, Circe).Shared codebase eliminates duplication and error propagation. Full-stack orientation lets you write both backend and frontend in the same language. Access to platform-specific libraries through facades.
To bridge Scala with platform-native APIs (JavaScript in the browser, C in the OS), Scala.js and Scala Native use a mechanism called facades — type declarations that tell the compiler how to emit correct calls to foreign APIs.
import scala.scalajs.js
import js.annotation._
// Facade for browser's window.alert
@js.native
@JSGlobal
object window extends js.Object {
def alert(msg: String): Unit = js.native
}
// Using a facade
window.alert("Hello from Scala.js!")
import scalanative.unsafe._
import scalanative.unsigned._
@link("m")
@extern
object libm {
def sqrt(d: CDouble): CDouble = extern
}
// Using a C binding
val result = libm.sqrt(16.0) // 4.0
Writing facades manually for large libraries is impractical. ScalablyTyped solves this by automatically converting TypeScript definition files (.d.ts) into type-safe Scala.js facades:
// With ScalablyTyped, NPM packages feel like native Scala libraries
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()
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")
| Framework | Architecture | Key feature |
|---|---|---|
| Laminar | Reactive, Observable-based | No external JS deps, targets DOM directly |
| Tyrian | Elm Architecture (Model-View-Update) | Integrates with Cats Effect |
Major Scala libraries are published for all three targets using %%% (instead of %%):
| Category | Libraries |
|---|---|
| Functional Programming | Cats, Cats Effect, FS2 |
| HTTP / APIs | Http4s, Tapir, Smithy4s |
| Serialization | Circe, uPickle |
| Testing | MUnit, Weaver |
Pure cross-platform projects cannot use general JVM ecosystem features (threads, filesystem) unless a cross-platform facade exists. Scala Native toolchain is still maturing: smaller library support, longer compile times (LLVM linking), and incomplete standard library coverage. Bundle sizes for Scala.js can reach ~1MB before tree-shaking.
Library-oriented, language-oriented, encapsulation-oriented, and DSL-embedding. tuProlog uses: library-oriented (Prolog calling Java via new_object/3, <-/2, returns/2; Java calling Prolog via Term/Engine API), and encapsulation-oriented (Java class as Prolog library; Scala wrapping Prolog theory).
new_object(+ClassName, +Args, -Ref) creates the object. Ref <- methodName(Args) calls a void method. Ref <- methodName(Args) returns Result captures a return value. Example: new_object('javax.swing.JFrame',[],F), F <- setSize(400,300), F <- setVisible(true).
Term is the root. Var for variables, Number (Int, Double, Long, Float) for numeric values, and Struct for compound terms (functors, lists). You build terms by constructing instances: new Struct("append", list1, list2, new Var("X")) creates append/3, or use engine.toTerm("[1]") to parse from string.
Call engine.solve(goal) to get the first SolveInfo. While info.isSuccess(), process it. Call engine.hasOpenAlternatives() to check if more solutions exist, then engine.solveNext() to get the next one. Break when no more alternatives.
mkPrologEngine takes a Prolog theory (as a string) and returns a function Term => LazyList[Term]. The LazyList wraps Prolog's multiple solutions as a lazy, potentially infinite stream. It integrates Prolog's search with Scala's functional abstractions, allowing map, filter, for-comprehension, and other collection operations over the results.
JVM, JavaScript (Scala.js), and Native (Scala Native). Configure via sbt-scalajs and sbt-scala-native plugins. Use sbt-crossproject with crossProject(JSPlatform, JVMPlatform, NativePlatform).crossType(CrossType.Full) to define a shared module and platform-specific modules.
A facade is a type declaration (using @js.native, @JSGlobal, etc.) that tells the Scala.js compiler how to emit correct JavaScript calls without generating implementation bytecode. ScalablyTyped automatically parses TypeScript .d.ts definition files and generates type-safe Scala facades, translating TS union types to Scala 3 union types, and mapping NPM packages to Scala packages.
The pure core (domain models, business logic) lives in a shared module using only cross-platform libraries. Platform-specific implementations (adapters) are in separate modules for each target: JDBC/Doobie for JVM, Laminar/Tyrian for JS, POSIX C interop for Native. Generic traits in the shared module define the interfaces; SBT handles cross-linking automatically.