Sunday, September 10, 2023

Combining the logical and imperative paradigm in a programming language

For my natural language execution engine I created a logical programming language to define the meaning of natural language phrases, and to specify the details of the execution. The language was a simplified version of Prolog, but I added expressions, conditionals, loops and mutable variables later, to make it easier to use. It got easier to use, but it also became more unpredictable to the application programmer (which is just me for the moment). 

The problem was that the logical constructs could create many bindings and one would just not expect that from an imperative language. A simple for loop would add multiple variables to the application and each goal of the body would create not just one set of bindings, but one for every iteration of the loop. The number of bindings could easily become unwieldy and extra care had to be taken to suppress them. This could be done by adding an extra set of mutable variables. It was possible, but very uncomfortable.      

After rethinking the language I decided to split the logical part and the imperative part and gave them each a separate language construct. 

Logical


A logical programming language has facts and inference rules, like this:

male(peter).
male(john).
female(mary).
parent(peter, john).
parent(mary, john).
father(X, Y) :- male(X), parent(X, Y).
mother(X, Y) :- female(X), parent(X, Y).

When a goal like father(X, john) is executed, X yields multiple variable bindings, one for each occurrence of male(X) in the system. If no males can be found, the inference rule stops, and the goal fails.

The essence of this paradigm is thus that each combination of variables in some scope, can have zero, one, or multiple bindings (values). A condition that can fail thus serves as a conditional for the rest of the goals. A condition that can have multiple bindings serves as a for loop for the rest of the goals.

My current syntax of the fact and inference rule are:

too_old(A) :- [ birth(A, Birth) A := age(Birth) A > 40 ]

The conditions can be only literals (birth(A, Birth)), assignments, and boolean expressions. It has no loops or conditionals. The variables are immutable and can hold multiple bindings.

This construct is great for definitions, and to interact with the database.

Imperative

A imperative programming language has functions (procedures) like this:

function hypothenuse(Width, Height) {
    WidthSquared = Width * Width
    HeightSquared = Height * Height
    Hypo = sqrt(WidthSquared + HeightSquared)
    return Hypo
}

Importantly, this paradigm yields only one binding for a function call. For loops and conditional much be named explicitly.

My current syntax of the function is:

hypothenuse(Width, Height) => Hypo {
    WidthSquared := Width * Width
    HeightSquared := Height * Height
    Hypo := go:sqrt(WidthSquared + HeightSquared)
}

Note that the returned value is specified by its variable, not its type, and that no explicit return statement is needed.

The body of the function can contain assignments, loops, conditional. It has no literals. All variables are mutable.

This construct is great for calculations.

Combining logical and imperative

To use logical literals in a function, use the explicit for-loop. This way it is clear that there can be multiple bindings, and any new variables have only the scope of the for loop.

for [size(E, S) E > 5] {
    /* statements */
}




On SQLAlchemy

I've been using SQLAlchemy and reading about it for a few months now, and I don't get it. I don't mean I don't get SQLAlchem...