apl.pl - Array-programming in ProLog

Download apl.pl

apl.pl is a small APL-like DSL implemented in Prolog.

This started as a small experiment for ovf based on conversations we had around mixing prolog and APL. This is just an initial experimental build of this with a prolog base. It turned into a compact little language that feels surprisingly pleasant to use.

It is not trying to be a full APL. The goal is narrower: keep the compact array surface, but let the semantics stay recognisably Prolog where that is useful. Arrays cross the boundary as plain Prolog values, = is unification, : is CLP(Z) equality, backtracking is preserved, and loaded predicates can be used directly as generators and verbs.

From an APL point of view, the interesting part is not just “APL syntax in Prolog”. It is that the surrounding Prolog system can enhance the array language:

It works in both Scryer and Trealla.

Downloads


Quick start

Load the language and the showcase:

?- use_module('./apl.pl').
?- use_module('./apl_showcase.pl').

Run the guided tour:

scryer-prolog -f apl_showcase.pl -g "run_showcase, halt"
tpl apl_showcase.pl -g "apl_showcase:run_showcase, halt"

Or evaluate expressions directly:

?- apl('+/ {3 3} # i 10', R).
R = [9,12,15].

Example run

?- apl('+/ {3 3} # i 10', R).
R = [9,12,15].

?- apl('^mask (1 + i 10) % 3 e 0 & ?mask m (1 + i 10)',
       [mask=Mask],
       R).
R = [3,6,9],
Mask = [0,0,1,0,0,1,0,0,1,0].

?- apl('?x + 3 : 10 & ?x', [x=X], R).
R = 7,
X = 7.

?- apl('?lhs = ?rhs', [lhs=pair(X,2), rhs=pair(1,Y)], R).
R = pair(1,2),
X = 1,
Y = 2.

?- apl('transpose ({2 3} # i 10)', R).
R = [[0,3],[1,4],[2,5]].

Those few lines already show most of what makes the system interesting:


Core ideas

The public boundary is deliberately plain:

There is an internal arr(Shape, Data) representation, but you only see it if you explicitly ask for apl_raw/2,3. For ordinary use, the results look like ordinary Prolog data.

The symbolic core is also deliberately small:

Everything else can come from ordinary Prolog predicates.


Prolog semantics inside APL

Unification is still unification

?- apl('?lhs = ?rhs', [lhs=pair(X,2), rhs=pair(1,Y)], R).
R = pair(1,2),
X = 1,
Y = 2.

Here = is real Prolog unification, not numeric equality. That makes arrays of symbolic terms feel natural instead of bolted on.

Relations can run backwards

?- apl('?x + 3 : 10 & ?x', [x=X], R).
R = 7,
X = 7.

: is CLP(Z) equality, so the expression can solve for the missing input instead of only evaluating left-to-right.

Intermediate APL values can be exported without breaking the pipeline

?- apl('^mask (1 + i 10) % 3 e 0 & ?mask m (1 + i 10)',
       [mask=Mask],
       R).
R = [3,6,9],
Mask = [0,0,1,0,0,1,0,0,1,0].

^mask Expr evaluates Expr, exports the plain Prolog value into Mask, and returns the same value unchanged so the expression can continue.


Prolog predicates as generators and verbs

Loaded predicates can be used directly:

That means the language can be extended from plain Prolog without adding new built-in symbols, which feels like the right tradeoff for a small system like this.

From the showcase:

?- findall(X, apl('0 between 5', [module=apl_showcase], X), Xs).
Xs = [0,1,2,3,4,5].

?- apl('square i 10', [module=apl_showcase], R).
R = [0,1,4,9,16,25,36,49,64,81].

Env-bound verbs also work:

?- apl('?f i 6', [module=apl_showcase, f=square], R).
R = [0,1,4,9,16,25].

This is one of the more useful integration points from an APL perspective: instead of trying to make the core language huge, you can let host Prolog provide domain-specific verbs.


Whole-array helpers

Most user predicates pervade scalar-wise over arrays. A few bundled helpers are whole-array operations because that keeps the code clearer:

Transpose:

?- apl('transpose ({2 3} # i 10)', R).
R = [[0,3],[1,4],[2,5]].

Compute a mask, turn it into indices with where, then select from the original values:

?- apl('^vals 1 + i 10 & ?vals @ where (?vals % 3 e 0)',
       [vals=Vals],
       R).
R = [3,6,9],
Vals = [1,2,3,4,5,6,7,8,9,10].

Select a column directly:

?- apl('1 col ({3 2} # i 10)', R).
R = [1,3,5].

These helpers matter because they keep the interesting examples readable. A pure ravel-index formulation would often be possible, but much less pleasant to look at.


Search first, then array programming

One of the nicest workflows here is:

  1. let Prolog generate a search space
  2. collect the solutions
  3. treat the result as an array
  4. use APL operators to analyse or transform it

From the showcase:

?- findall(P, apl('pair_sum 10', [module=apl_showcase], P), Ps).
Ps = [[1,9],[2,8],[3,7],[4,6],[5,5],[6,4],[7,3],[8,2],[9,1]].

Now that search result is just a matrix:

?- findall(P, apl('pair_sum 10', [module=apl_showcase], P), Ps),
   apl('+/ ?pairs', [pairs=Ps], R).
R = [45,45].

Transpose it into explicit columns:

?- findall(P, apl('pair_sum 10', [module=apl_showcase], P), Ps),
   apl('transpose ?pairs', [pairs=Ps], R).
R = [[1,2,3,4,5,6,7,8,9],[9,8,7,6,5,4,3,2,1]].

Filter rows using a computed column:

?- findall(P, apl('pair_sum 10', [module=apl_showcase], P), Ps),
   apl('^left 0 col ?pairs & ?left % 2 e 0 m ?pairs',
       [pairs=Ps],
       R).
R = [[2,8],[4,6],[6,4],[8,2]].

This is where the combination starts to feel genuinely fun. Prolog generates structured data; the APL side then reshapes, filters, scans, masks, and reduces it compactly.


The APL is also a Prolog term

The parser is exposed:

?- apl_ast('square i 6', AST).
AST = monad(ident(square),monad(i,num(6))).

So expressions can be inspected, rewritten, and re-evaluated:

?- apl_ast('square i 6', AST),
   AST = monad(ident(square), Arg),
   apl_eval(monad(ident(inc), Arg), [module=apl_showcase], R).
AST = monad(ident(square),monad(i,num(6))),
R = [1,2,3,4,5,6].

That is a very Prolog-specific advantage. The little APL language is not just something you call; it is also data that the host language can analyse or transform.


Small but deliberate

apl.pl is intentionally small.

The point is not to recreate all of APL inside Prolog. The point is to let a Prolog programme grow a terse array notation without giving up the logic-programming properties that make Prolog interesting in the first place.

For me, that is the sweet spot:

If you just want to get a feel for it, start with apl_showcase.pl and run run_showcase/0. That file is the friendliest way in c: