Skip to content

Using Pengines.Client

Dave Curylo edited this page Sep 25, 2018 · 4 revisions

An introduction to working with Prolog from F# using the Pengines.Client.

Prolog Fundamentals

Logic Programming

Logic programming made entirely by defining logical statements. In Prolog, you define things that are true and send the Prolog engine off to find solutions that fit your query that are true statements.

Terms

The language is conceptually simple. The fundamental building blocks are terms, which represent everything in an application. All of the data is defined in terms, operators are terms, predicates and logic are also terms.

type Operator = 
    | Infix of Term * string * Term
    | Prefix of string * Term
    | Postfix of Term * string

and Term =
    | Atom of string
    | Number of decimal
    | Variable of Name:string
    | ListTerm of Term list
    | CompoundTerm of Functor:string * Terms:Term seq
    | DictTerm of Map<string, Term>
    | Operator of Operator

An atom is effectively a fixed string that you would use in a larger predicate. Many common operators are also just atoms that have been assigned to an operator.

  • hello
  • 'Hello, my name is Jeff.'
  • super_power
  • +

A number is a constant number, and may be an integer or floating point number.

  • 42
  • 99.9999999

A variable is a reference to solutions to a Prolog query. It will bind to whatever it possibly can to make a query true. It's very important to keep in mind that a term starting with a capital letter is considered a variable. If you want a capitalized string to be a prolog atom, surround it in single quotes.

  • Who
  • Total
  • WHATEVER

A list is a an unordered set of terms, with a head and a tail, or just an empty list. Unlike F#, a list in Prolog can contain different types, although this is because it's a dynamically typed language, and not anything special about this list itself.

  • [a, b, c, d, e]
  • [cat]
  • []

A compound term is a named structure. It could be thought of as a named tuple or, maybe more appropriately, a functor with some number of arguments (its arity).

  • birthplace(melissa, sacramento) - in this case, birthplace has an arity of 2, and is often represented as birthplace/2
  • length(2) - length has an arity of 1.
  • width(5) - width also has an arity of 1.
  • rectangle(length(Length), width(Width)) - rectangle has an arity of 2, called rectangle/2.
  • area(rectangle(length(Length), width(Width)), Area) - area has an arity of 2, so area/2.

An operator is a term that has been defined as an operator to shorten the notation and make the language more natural by having prefix, postfix, or infix notation. For example, instead of writing +(40, 2), the + has been defined an an infix operator, so it can be used as 40 + 2. Here are some common operators:

  • > - logical greater than
  • + - addition
  • , - logical conjunction, also known as and
  • ; - logical disjunction, also known as or
  • . - terminates a statement
  • :- - if

That's it. The whole language is terms, and you can represent everything in prolog with these types of terms. There are whole frameworks full of predefined terms, like builtin operators and predicates, but these are the fundamental building blocks for the entire language.

Facts and Rules

A fact is a means of defining something that is true for Prolog. These are all some facts about occurances on a fateful afternoon in Mos Eisley, all defined as compound terms followed by period to tell prolog that this ends their definition:

shot(han).
shot(greedo).
died(greedo).

Facts in and of themselves are not so interesting. You can make a dictionary to look things up, and so maybe we want to solve for people that shot, maybe to find out who definitely had a gun. We define a compound term, has_blaster/1 and pass a variable, Who to it. This variable will bind to whatever is defined within the rule. In this case, we define that it should bind with whoever shot:

has_blaster(Who) :- shot(Who).

Now we've taken some facts, and we've made a rule out of them. We know that someone has a blaster if they managed to shoot. We can ask Prolog:

?- has_blaster(Who).
Who = han ;
Who = greedo.

Prolog figured out the (trivial) solutions for who has a blaster based on the facts it was given and a rule, which defines a relationship between those two pieces of data.

There's also another fact, because greedo and han both shot, but also, died(greedo). There's no way to really know for sure, but these two aimed blasters at each other, both shot, but only one died. It's reasonable to say that the impact of the first shot disturbed the aim of the second shot so it missed, meaning that whoever died fired the second shot. Again, there's no way to know for sure, but let's write a predicate on what we know and let Prolog solve for shot_first(Who):

shot_first(Who) :-   % a compound term, shot_first/1, a variable Who, and the if operator
    shot(Who),       % a compound term, shot/1, a variable Who, and the and operator 
    not(died(Who)).  % a compound term not/1, a compound term died/1, the variable Who, and a period to end the definition.

We've defined a predicate, the shot_first functor, with an arity of 1. Let's solve for it, because I've always wanted to know.

?- shot_first(Who).
Who = han .

We've given our Prolog engine some knowledge in the form of facts and rules, and it can start to use the knowledge to make inferences.

These examples were not too complex, certainly something that could have been done just as easily in F#.

> let shot_first shooters whoDied =
    shooters |> List.find (fun s -> s <> whoDied);;

> shot_first ["han"; "greedo"] "greedo";;
val it : string = "han"

Wait, that was really easy in F#. Why use Prolog? What happens in F# when there is more than one solution? We keep a list of results and operate on all items in the list. If our operations on that list result in some more lists for each, we just keep working through all that data looking for our solution. Before long, it can get pretty complex to find a solution amongst lists and lists of possibilities. Let's try it!

Pengines

Pengines is a SWI-Prolog module that allows prolog code to be executed in a sandbox, in it's own thread with a dedicated dynamic clause database and queues for processing requests and returning responses. This makes it possible to build a distributed applications running in a Prolog engine. An HTTP server runs on top of this to serve as a back end for applications using the Pengines.Client. The Pengines server can run on Windows, Linux, or macOS.

Limitations

  • The sandbox only loads a few modules by default. All the standard Prolog modules are there, but modules like clpfd (Constraint Logic Programming over Finite Domains) are not loaded because they are not considered "safe" because they may have side effects like accessing the filesystem or loading foreign extensions. However, they may be added, but you'll need to review them and understand the context in which you use them.

Alternatives

  • Load SWI-Prolog by the native interface using P/Invoke. This is relatively complex due and also subject to threading complications. You'll need to ensure there is no concurrent use of various parts of the native SWI-Prolog infrastructure.
  • Embedded Prolog - there are implementations of Prolog in other languages and frameworks that are suitable for embedding. Many are missing common predicates and optimizations that are in SWI-Prolog.
  • Command line processes - SWI-Prolog goals can be evaluated directly on the command line, albeit there can be scalability issues with a process per request, and it's quite possible for simple prolog applications to recurse indefinitely. This infinite recursion is prevented by Pengines, but a CLI process would run until overflowing the native stack.
Clone this wiki locally