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.
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].?- 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:
^nameThe 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:
i, #, @,
m, +/, +\=, :, !,
&, |e and zEverything else can come from ordinary Prolog predicates.
?- 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.
?- 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.
?- 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.
Loaded predicates can be used directly:
gen calls gen/1inc X calls inc(X, Result)A between B calls
between(A, B, Result)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.
Most user predicates pervade scalar-wise over arrays. A few bundled helpers are whole-array operations because that keeps the code clearer:
transpose/2where/2col/3Transpose:
?- 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.
One of the nicest workflows here is:
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 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.
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: