For example, with Scryer Prolog and its clpb library, we can define:
We can enumerate all solutions with labeling/1:
:- use_module(library(clpb)). nand_gate(X, Y, Z) :- sat(Z =:= ~(X*Y)). xor(X, Y, Z) :- nand_gate(X, Y, A), nand_gate(X, A, B), nand_gate(Y, A, C), nand_gate(B, C, Z).
The solver also includes a feature that lets us algebracially show the defined relation as a function of X and Y by posting the query:
?- xor(X, Y, Z), labeling([X,Y]). X = 0, Y = 0, Z = 0 ; X = 0, Y = 1, Z = 1 ; X = 1, Y = 0, Z = 1 ; X = 1, Y = 1, Z = 0.
?- xor(x, y, Z).
This makes it immediately obvious that the circuit models XOR, as intended!
clpb:sat(Z=:=x#y), ... .
The section on Constraint Logic Programming starting at page 171 contains more information about this paradigm. You can make constraint solvers available as libraries also in other programming languages. However, they blend in especially seamlessly in logic programming languages like Prolog, because constraints already play such a prominent role in these languages, and generalizations and specializations of programs and queries come naturally: In fact, the built-in predicates (=)/2 (unification) and dif/2 (disequality of terms) can also be regarded as constraints (over Herbrand terms, H). Prolog itself can therefore be regarded as a particular instance of constraint logic programming, CLP(H).
I also highly recommend the chapter on Logic Grammars starting at page 185 in the magazine: Convenient and efficient text processing was the original goal of Prolog and the reason for its development. Prolog originates from Q-systems, which were introduced by Alain Colmerauer for the translation of weather reports between French and English. Nowadays, virtually all Prolog systems ship with Definite Clause Grammars (DCGs), one of the most attractive mechanisms to describe and reason about sequences in Prolog, which can be used for describing and analyzing lists of characters and hence text as a special case.