Have you ever contemplated the possibility of having your computer solve your problems for you, without you having to tell it how? If your answer is a frantic “yes!”, you’re in luck: this is a thing, and this thing is called logic programming.

We start writing our logic pragram by declaring the facts that we know. Let’s consider a typical, red-haired, magical family:

% main.pl

% Facts
father(arthur, ron). % Arthur is a father of Ron
father(arthur, ginny). % Arthur is a father of Ginny
mother(molly, ron). % Molly is a mother of Ron
mother(molly, ginny). % Molly is a mother of Ginny

In case you’re wondering, this is the Prolog, one of the first and most prominent logic programming languages.

Next, we declare some rules:

% main.pl

% Rules
% X is a parent of Y when X is a father of Y
parent(X, Y) :- father(X, Y).
% In addition, X is a parent of Y when X is a mother of Y
parent(X, Y) :- mother(X, Y).
% X is a sibling of Y when X and Y share the same parent Z
sibling(X, Y) :- parent(Z, X), parent(Z, Y).

After stating our facts and rules, we can query our program for answers. Let’s ask Prolog a question:

$ swipl main.pl
% ?- sibling(ron, ginny). # Are Ron and Ginny siblings?
% true.

That’s indeed the right answer!

You might ask yourself: What’s remarkable about this? Let me answer this question with another question: Have you noticed that we didn’t write an imperative procedure that checks whether Ron and Ginny are siblings? Because we haven’t.

We did something much simpler; we just gave Prolog some facts and rules, asked Prolog a question, and then Prolog navigated the search space for an answer on its own.


We can experience some of Prolog’s awesomeness in JavaScript using LogicJS:

// family.js

import { lvar, run, and, or, eq } from "logicjs";

const father = (x, y) => {
  const facts = [
    and(eq(x, "arthur"), eq(y, "ron")),
    and(eq(x, "arthur"), eq(y, "ginny")),
  ];

  return or(...facts);
};

const mother = (x, y) => {
  const facts = [
    and(eq(x, "molly"), eq(y, "ron")),
    and(eq(x, "molly"), eq(y, "ginny")),
  ];

  return or(...facts);
};

const parent = (x, y) => {
  return or(father(x, y), mother(x, y));
};

const sibling = (x, y) => {
  const z = lvar();
  return and(parent(z, x), parent(z, y));
};

const x = lvar();
const result = run(sibling("ron", x), x, 2); // Who are the siblings of Ron?
console.log(result); // ["ron", "ginny"]

It works! But the query gives us two answers: Ginny and Ron. According to the rules we specified, Ron is technically a sibling of Ron; since Ron shares at least one parent with Ron.


We can also do some constraint programming with LogicJS. Let’s write a program to find out two numbers which:

  • are between 1 and 100
  • yield 20 when added
  • yield 96 when multiplied
// math.js

import { lvar, run, and, between, add, mul } from "logicjs";

const x = lvar();
const y = lvar();

const constraints = [
  between(1, 100, x),
  between(1, 100, y),
  add(x, y, 20),
  mul(x, y, 96),
];

const result = run(and(...constraints), [x, y]);
console.log(result); // [[8, 12], [12, 8]]

Right again! The query gives us 8 and 12, as well as its mirrored result 12 and 8.

Elegant, right? No procedures, no if conditions, no for loops. This, in my opinion, is one of the most beautifully succinct ways to solve such a problem.


If you’d like to have more fun with logic programming, you can attempt to solve a Sudoku puzzle, fill in Pascal’s triangle, or rebuild Deutsche Bahn’s scheduling system.


Thanks to the amazing Thomas Bartel for giving valuable feedback about the writing style of this blog post ❤️