% find the list resulting from appending two lists
append([1],[2],C). %% => C = [1,2]
% find the prefix given a suffix
append(A,[2],[1,2]). %% => A = [1]
% find the suffix given a prefix
append([1],B,[1,2]). %% => B = [2]
% find all prefixes and suffixes
append(A,B,[1,2]).
A = [], B = [1,2];
A = [1], B = [2];
A = [1,2], B = [];
false ;; no more results
The first line is what most programmers new to Prolog will be used to, a function performs a computation in a forward fashion (here: given two knowns, calculate something). But all the others come along "for free" (if you write it correctly, which isn't too hard once you've learned a bit of the language and style).My practical use for this was with a (prototyped, not fielded) scenario generator. Using the rules (specification for the protocol) I could generate arbitrary scenarios. I could constrain them by filling in some variables, but leaving others blank. And I could take an existing scenario and determine if it was even valid. Using append again:
append(A,[3],[1,2]). %% => false
Now, in my situation instead of a hardcoded [1,2] I had a recording of an exchange between nodes, and I could use this to get an answer to why it didn't conform to the spec. One program that could be used in many different ways.The other reason to use Prolog, this was a much higher level of abstraction to work with than what we ended up using. It was much shorter, but less familiar to others. So we ended up with a C# + SQL solution where we had to write each variation of the program (generate, test for validity, identify problems).
It's a constraint solving language, linear problem solving language, a database query language, a parsing language, and an expert systems language. All unified (sorry that's a pun) transparently in a single language suitable for general purpose programming instead of half a dozen partially interoperable DSLs.
'make' is pretty good example, but 'package manager' might be more relevant to people. instead of having explicit control flow you have rules that say 'this is the case if these other things are the case', and they get triggered to evaluate implicitly (and recursively).
variables and values in declarative languages are implicitly quantified to be sets of objects, so we say 'for all X where' .. instead of '_the_ X where', which makes it similar to SQL.
actually there you go - pretend this is an embedded database - that's clearly useful, except with a better query language.
Something I've personally used it for was navigating the parse tree of some T-SQL I was analyzing (it was 100k+ LoC so manual analysis was out of the question). There is a pretty good C# library for parsing T-SQL, but it is unwieldy to use since it relies on visitors and has tons of different typed nodes with a huge inheritance hierarchy. I ended up exporting this to JSON including type information and initially used jq for analysis. However, even that ended up being hard to manage so I switched to Prolog and it was way easier to write. In terms of this project, I particularly liked that the code was so dense and expressive that I could write the program by first testing things out in the REPL.