Now, a lot of people, when they hear the term "constraints", think of arithmetic inequalities. And indeed, there are "Constraint Logic Programming" extensions to Prolog that support such constraints. But that's not what I'm referring to. Even in the absence of such extensions, Prolog is a constraint language, but the constraints are structural rather than numerical.
Let's take a simple example, the Prolog code that defines list appending:
append([], X, X).
append([X|Y], Z, [X|W]) :- append(Y, Z, W).
This says, first, that 'append' is satisfied if its first argument is empty and the second and third are equal; and secondly, that it's also satisfied if the first and third argument have the same head, and 'append' is satisfied recursively on the tail of the first argument, the second argument, and the tail of the third.Prolog allows any of those arguments to be inputs, and any to be outputs. The way this works is that variables in structures function as named holes to be filled in. So while you can use 'append' as a function to append two lists, like this:
? append([3], [7], X).
X = [3, 7]
you can also use it as a predicate: ? append([3], [7], [4]).
no
Here "no" means Prolog couldn't find any way to satisfy the constraint. Or, you can use it "backwards": ? append(X, [7], [4, 7]).
X = 4
Prolog doesn't even know what's normally an "input" and what's an "output"; it just searches for a solution to the constraints. It can even construct multiple solutions: ? append(X, Y, [4, 7]).
X = [], Y = [4, 7];
X = [4], Y = [7];
X = [4, 7], Y = []
Here's the really powerful part. These solutions are produced one at a time. Some larger goal can be consuming them, such that if it fails, Prolog will try the next one automatically. Thus it's very easy to put together a large, complex constraint and have Prolog search for a solution.The catch, as others have mentioned, is that it does that using a rather naive depth-first search. Even though, at the lowest level, it tries each alternative very quickly, there can easily be a combinatorial explosion of alternatives. To use the language effectively requires the programmer to learn strategies to avoid these explosions. So while in small examples the language seems very declarative, and indeed a lot of code can be written without too much attention to its procedural behavior, eventually this breaks down and you have to think about it procedurally as well.
Prolog was intended to be a way to program using logic. It did this in a way by encoding facts in a manner not dissimilar to a relational database; logical statements about the world that resembled functions using Horn clauses (which you can think of as mathematical functions, but applied to logic); and unification, which is what links all this together. Unification is computation via the idea that you have a set of data, a bunch of logical statements, and you want to find everything in the data that satisfies those logical statement.
If you know SQL, Horn clauses should sound a lot like materialised views.
As to why it never really went mainstream, it's a matter of wrong place, wrong time. It didn't help that the language needed a checkpointing mechanism (referred to as 'cuts', and represented by '!') because the unification mechanism could get confused and do much more work than it needed to, so developers needed to put cuts in place to tell the interpreter where it could avoid backtracking to try more solutions within a Horn clause.
If you want to get a taste for it, maybe give this a try: https://github.com/norswap/prolog-dry
Prolog is one of a small number of languages that people should learn even if they never use it in a practical way.
Prolog backtracks when there are more solutions to a goal (think of a goal as a query). Not because "unification gets confused". Unification doesn't have anything to do with backtracking, rather backtracking is a mechanism to represent the natural nondeterminism of logic theories, where multiple variable instantiations ("bindings") might satisfy a formula.
So, when you enter a query at the Prolgo top-level (the console) Prolog will try to find a set of variable bindings that refute your query in the context of your loaded program. If it can, it will report "false", i.e. the query cannot be satisfied in the context of the program. If it can't, it will report "true", i.e. the query can be satisfied in the context of the program and it will also report the variable bindings that make the query true. If there are more than one sets of bindings that make the query true, you can ask for more solutions. At that point Prolog will backtrack and give you more solutions.
However, there is nothing forcing your program to be written so as to produce multiple solutions to a query. It's entirely up to you and how you write your program. You can write a program so that it's deterministic (succeeds or fails exactly once) or not. The cut (!/0) is one way to force a program to become deterministic, but again that's not because anything gets confused. It's because sometimes it's hard to write a program so as for it to be deterministic. And sometimes you just have no idea why your program backtracks and you pepper your code with cuts, hoping it will stop.
Which of course happens less and less as you get to understand how the language works (and why it's a bad idea to put cuts everywhere in the first place).
(It's a bad idea because it means you don't understand how your own program works. Nine times out of ten you don't need to use it when you think you do).
(But I still use the cut all the bloody time because it's convenient).
I know that, and I know the difference between a red and a green cut. However, I was targeting my explanation towards somebody who is completely unfamiliar with the language.
Prolog was (theoretically) a practical declarative programming language. You describe what you want done, and it "figures out" what to do, as opposed to imperative, where you simply tell the computer each step to do.
That's something of an exaggeration in practice, but yet, there was something to it. A typical program was actually doing depth-first search behind the scenes, so "try this, or else try this, or else try this" algorithms were very easy to express in the language.
As an aid to that, it had unification, which you can think of as pattern matching on steroids. You could write a rule like
something([A, [1, X]]) :- yada(A, X)
which would only match if the argument to 'something' was a list the second element of which was another list that started with 1, and so on. It's far more powerful than this, but it's been many years, and I've forgotten most of it.
As for the cons, implementations of the day were relatively inefficient, although Quintus was fairly good.
The killer problem, in my mind, though is that it didn't really deal well with backtracking through huge data structures. If you were trying to write a bit-blitting algorithm on large arrays, for example, and expecting to backtrack a lot, it just plain wasn't going to work well.
Not sure that helps much. I miss Prolog a lot, but it's hard to see a path forward for it. Even somewhat similar languages like Haskell will probably never get any real traction. Or even Ocaml/F#, which are far more practical.
I suppose the closest we have today are really good Makefiles. Done well, this captures just a bit of Prolog, in a very limited domain.
Part of writing efficient Prolog is doing things to avoid such backtracking (by adding cuts or rearranging code).
Bit-blitting is just an example I made up for "large-scale manipulation of array data". There are such cases where backtracking would likely make more sense, though I'm not thinking of an obvious one right now.
The exception would be Turbo Prolog, but it was a rather restrictive subset of the language. Fast as hell, though.
Further more, it is missing many things that you come to expect in a modern programming language - arrays, good numerical performance, etc.
The way it works though allows you to do some things quite beautifully. For example, since every function you right can be run either forwards or backwards (solving for any one unknown amongst the arguments or result), you can often write one single piece of logic for parsing that can just be run in reverse to do serialization (or vice versa).
The main reason, though, that it (or it's progeny like Mercury) is not mainstream is really the same as the reason that Haskell and its derivatives are not. It's just too different from the more widely known algol/simula langages, and the library ecosystem is comparatively poor.
Those depend on the interpreter. For example, Gnu Prolog has full support for arrays. Swi-Prolog has good support for rational numbers. Constraint libraries like library(clpfd) in Swi-Prolog simplify numerical computations. etc.
About numerical support in particular, the problem is that representing arithmetic in first order logic is not straightforward because it doesn't have a concept of a "number"- so what a "number" is, must be defined before arithmetic can be done (and in fact, to some extent, this was the purpose of FOL in the first place). Additionally, Prolog slightly departs from first order logic semantics in that it doesn't have a clearly defined concept of "function" (by contrast in FOL "arguments" of predicates are variables, constants or functions). Instead, Prolog "cheats" and allows predicates to be passed as arguments to predicates. However, for performing arithmetic operations, Prolog defines a predicate is/2 that takes as arguments two "mathematical expressions", where an expression is either a number ...or a function (so a function; because constants are functions with 0 arguments). For example, to add two numbers you would write:
?- A is 1 + 1.
A = 2.
This is clunky and messy and ends up hurting the eyes and the brain, even though there's no real problem with performance in particular (in most Prolog systems there's a low-level back-end that handles that anyway). The constraint library clpfd that I mention above exists to smooth out this impedance mismatch between the declarative paradigm and the wonky "functions in a language without functions" of mainline Prolog. It's billed as "declarative integer arithmetic" (and does the job well) (once you get used to it).Anyway it's not that there's no good support for numerical computations. It's just ...weird. Even in the context of, you know, Prolog.
def area(base, height):
return (base * height) / 2
This is a function which takes the base and height as inputs, and returns the area as an output, and that is all it can do. You'd never think that's "all" because what else would you expect it to do, but what you wrote down in code? In Prolog, instead you can write this: :-use_module(library(clpfd)).
triangle_area_sides(Area, Base, Height) :-
Area #= (Base * Height)//2.
This is not a function, it doesn't take any parameters or return anything. It's a rule which says how the Area, Base and Height values are related, and an import for a module which handles numeric calculations. Now you can ask the Prolog rules engine to solve for any missing value(s):- Given a base and a height, what is the missing Area value? (Same as the Python version)
- Given an Area and a Base, what is the missing Height?
- Given an Area and a Height, what is the missing Base length?
- Given an area, a base and a height, is that a valid triangle?
- Given an Area, generate some / many Base and Height combinations which make a triangle of that area.
- Given a Base, generate some / many Area and Height combinations which make a triangle of that base length.
- Given a Height, generate some / many Area and Base combinations which make a triangle of that height.
- Given nothing, generate some valid triangle sizes.
- Given some values but not others, can it be solved at all (true/false)?
Pros: that's much more capable than the Python function of roughly the same code length. Multiple logic statements like this can make a lot happen in a little code.
Cons: it's very different from imperative programming, it makes IO and state interactions a bit more difficult, it's easy to get the Prolog engine stuck generating infinite combinations or searching enormous search spaces and never finishing, debugging what it's actually doing and what is going wrong is a very different skill from imperative languages.
And years of college courses using Prolog as a "force them to deal with recursion", leaving people with the impression that Prolog is incapable of anything good, and what it can do is slow and needlessly difficult.