Goodnight Wiki / Logic Programming

Logic Programming

There's a strange inversion at the heart of logic programming: you don't tell the computer how to find an answer, you tell it what an answer looks like, and it figures out the how. This sounds like a magic trick, and in a way it is -- but it's also one of the oldest ideas in computer science, going back to the early 1970s. The fact that it remains niche despite being genuinely powerful tells us something interesting about what programmers actually want from their tools.

The Core Idea: Relations, Not Functions

In most programming, you write functions: given inputs, produce outputs. In logic programming, you write relations: statements about how things are connected. The canonical example is Prolog's mortal(X) :- man(X), which doesn't say "given X, compute whether X is mortal." It says "X is mortal if X is a man." The direction of computation is left open. You can ask "is Socrates mortal?" and get yes. You can ask "who is mortal?" and get a list.1

This bidirectionality is what makes relational programming feel magical. William Byrd's work on miniKanren demonstrates the extreme case: a relational interpreter -- an interpreter written as a relation rather than a function -- can not only evaluate expressions, but also generate valid expressions given a desired result. Run the interpreter "backwards" and you get program synthesis for free.2

Mozart/Oz makes the mechanics particularly explicit. Where Prolog uses automatic depth-first search, Oz separates the concerns: you write your relations using choice blocks that create choice points, then run them inside a search engine that can implement any strategy you want. This separation is elegant. It means you can visualize the entire search tree with the Explorer tool, switch from depth-first to breadth-first, or build custom search strategies -- all without touching your logic.2

The Constraint Programming Connection

The real power of logic programming shows up when you move from pure unification to constraint solving. David Nolen's core.logic in Clojure demonstrates this beautifully: a puzzle that takes 430ms with an optimized list comprehension runs in 84ms with finite domain constraints, without any manual goal reordering.3 The insight is that a logic puzzle is really a finite domain problem in disguise -- "surprisingly similar to solving sudoku."

Answer Set Programming (ASP) pushes this further into territory that looks less like traditional logic programming and more like declarative specification. In ASP, you specify constraints and let a solver find stable models. The syntax is alien at first -- 1 { x(X,Y,N) : val(N) } 1 :- val(X;Y) says "for each X,Y pair, exactly one N value must be assigned" -- but the concision is remarkable. A Sudoku solver in ASP is essentially just the rules of Sudoku, stated directly.4

The biggest practical difference between ASP and Prolog is termination: ASP's computational process always terminates, unlike Prolog's query evaluation which can loop forever. This makes ASP much more suitable for NP-hard search problems, which is its primary niche. The Potassco tools (gringo, clasp, clingo) have been dominant in ASP competitions for good reason.

Mercury: What If Prolog Had Types?

Mercury sits at a fascinating intersection: it's Prolog with Haskell-grade type checking. Every predicate has a declared determinism category -- det (exactly one solution), semidet (zero or one), multi (one or more), nondet (zero or more) -- and the compiler enforces this statically. If you declare a predicate as det and the compiler can prove it might fail, you get a type error.1

This sounds like a small addition, but it transforms the programming experience. In Prolog, any predicate might silently fail or produce unexpected multiple solutions, and debugging this is a nightmare. In Mercury, the type system catches these errors at compile time. The price is verbosity -- Mercury's hello world is notably longer than Prolog's -- but the language's designers are explicit about this tradeoff: "Mercury was designed to be useful for the development of large and robust real-world applications."

The deeper point is that Mercury demonstrates how type systems can be applied to declarative paradigms. Prolog's untyped nature isn't an essential feature of logic programming; it's an accident of history that makes the paradigm harder to use at scale.

Higher-Order Logic Programming

One criticism of classic Prolog is that it feels first-order -- you can't easily pass predicates around the way you pass functions in Haskell. Markus Triska's work on higher-order predicates in modern Prolog shows this isn't true anymore. The call/N family of predicates allows you to invoke goals dynamically, and maplist lets you lift any relation on single elements to lists, in all directions.5

The if_/3 meta-predicate is particularly clever: it combines desirable declarative properties (working in all directions, maintaining logical purity) with good performance by using reified conditions rather than Prolog's traditional impure negation-as-failure. This matters because the standard findall and \+ (not-provable) predicates destroy monotonicity -- adding a clause can cause previously-succeeding goals to fail, which is philosophically terrible for a declarative language.

Why Isn't Logic Programming More Popular?

The honest answer is that most programming tasks aren't search problems. When you're building a web app or a data pipeline, you know exactly what steps you need, and the function-call model maps naturally. Logic programming shines for constraint satisfaction, symbolic reasoning, and problems where the search space is complex but the constraints are expressible -- scheduling, configuration, type inference, certain kinds of AI planning.

There's also a practical barrier: debugging logic programs is hard. When your Prolog program gives wrong answers, the problem might be in your logic, your search strategy, or subtle interactions between the two. The abstraction that makes logic programming powerful -- "I don't care how you find the answer" -- is the same abstraction that makes debugging difficult, because now you do care how it's searching, and the how is hidden.

Still, I think the paradigm is genuinely underrated, especially as an embedded DSL. Core.logic in Clojure, miniKanren in Scheme, and similar embeddings let you drop into relational mode for the parts of your program that are naturally relational, without forcing the entire application into that paradigm. That's probably the right future for logic programming: not as a standalone language, but as a tool in the kit for the problems it fits.

Footnotes

  1. Mercury Crash Course by J Fondren -- source 2

  2. Relational Programming in Mozart/Oz by Chris Double -- source 2

  3. Logic Programming is Underrated by David Nolen -- source

  4. A first look at Answer Set Programming by Hakan Kjellerstrand -- source

  5. Higher-order Predicates by Markus Triska -- source

Open in stacked reader →