Good day.
I have been wondering for some time about whether to bring this up, but in the end I decided that yes, it may be worth it.
In the SWI Prolog manual, we find the page for library “yall” (which stands for “Yet Another Lambda Library”).
The library provides a very interesting capability, namely to write anonymous wrappers around predicate calls. The library implements Logtalk’s “lambda expressions” syntax. Indeed, it has been authored by Paulo Moura, Logtalk designer/developer, and Jan Wielemaker, SWI-Prolog maintainer. See also: Logtalk Lambda Expression.
Well, I am a bit bothered by the terminology.
The yall
library documentation says:
Prolog realizes high-order programming with meta-calling. The core predicate of this is
call/1
, which simply calls its argument. This can be used to define higher-order predicates such asignore/1
orforall/2
. The call/N construct calls a closure with N-1 additional arguments. This is used to define higher-order predicates such as themaplist/N
family orfoldl/N
.
The Logtalk glossary defines “closure” in this way:
A callable term (i.e. an atom or a compound term) passed to a meta-predicate call where it is extended with additional arguments to form a goal called by the meta-predicate.
I really feel that “closure” is used inappropriately here.
The above does not describe a closure but a bunch of parameters for an indirect call:
- A tuple
- with the name of the predicate to call and
- the arguments to be used for the call.
This is similar to a Java “method call by reflection” (say), only a lot less horrible and with logical variables (which are really references to shared, versioned tree structures, what I like to call “blackboards”).
The term “closure” is also used in several other places in the SWI Prolog documentation, apparently as a synonym for “goal” (which is of course a term proper to logic programming, unlike “closure”). When you search for closure
, you get a few hits. (But you don’t get library yall
or a few other pages that one would expect. Is there a problem in indexing?) For example:
prolog_unlisten/2
: Remove matching closures registered with prolog_listen/3.prolog_listen/2
: Call Closure if an event that matches Channel happens inside Prolog.prolog_listen/3
: Call Closure if an event that matches Channel happens inside Prolog.
The above are not closures either. They are goals for event handlers.
Wikipedia has a rather good definition of Closure:
In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.
Note in particular that “closure” is a concept inherent to Functional Programming. Using it in Logic Programming is confusing.
So what are the problems?
- Prolog is not a language based on functions (barring whatever is on the right-hand side of the
is
operator). Perforce there are no constructible and returnable functions. Perforce there no closures. - Predicates are not “first-class citizens”. You cannot define them, pass them, and in particular return them. Perforce there are no predicates equivalent to closures.
- Although you might be able to laboriously construct predicates by building terms for the predicate’s clauses and
assert
those into the database. - You can of course call predicates indirectly by their name (used in the same way as a function pointer by having
call/x
lift a term to the “predicate plane” and call that. Which is what we are talking about.
- Although you might be able to laboriously construct predicates by building terms for the predicate’s clauses and
- There is no context to “close over”. Prolog is lacking in contexts (maybe a disadvantage). It has the global namespace and the context of the clause. Nothing else. There are no variables to look up in a context. The clause’s context does not survive return.
So no closures. Just indirect calls.
How would a real “logic programming closure” look like in Prolog? Here is a bad attempt:
- Uppercase lambda Λ marks predicate logic variables, as well as the “anonymous clause head”.
- Arrow down ↓ marks a copy-term operation from a term in the clause context to a term in the anonymous clause context.
create_poly_in_x(C0,C1,C2,ΛP) :-
ΛP is (Λ(X,R) :- R is ↓C0*X*X + ↓C1*X + ↓C2)
?- create_poly_in_x(4,4,6, ΛP), ΛP(12,R).
R = 630.
This would a dog when backtracking. And how does one go about when actual predicates are involved, not just “predicaty-looking” functions? But that’s for another time.
I would argue that what library yall
provides is not even a lambda expression, which is another thing with a clear meaning only in the context of functional languages:
Again, from Wikipedia, the Anonymous Function or “Lambda Expression” (colloquially, “Lambda”)
In computer programming, an anonymous function (function literal, lambda abstraction, or lambda expression) is a function definition that is not bound to an identifier. Anonymous functions are often arguments being passed to higher-order functions, or used for constructing the result of a higher-order function that needs to return a function. If the function is only used once, or a limited number of times, an anonymous function may be syntactically lighter than using a named function. Anonymous functions are ubiquitous in functional programming languages and other languages with first-class functions, where they fulfill the same role for the function type as literals do for other data types.
What yall
provides, however, is the possibility to define shims or wrappers around predicates:
From Shim:
“In computer programming, a shim is a library that transparently intercepts API calls and changes the arguments passed, handles the operation itself or redirects the operation elsewhere. Shims can be used to support an old API in a newer environment, or a new API in an older environment. Shims can also be used for running programs on different software platforms than they were developed for.”
There is also the term wrapper library, which is basically the same thing.
In fine, I do think the use of the term “closure” should be banished until the next generation of LP languages.
Addendum: Example
Let’s take a simple polynomial, a function of two variables:
- In lambda notation:
p := λx.λy.x³+y²
- Or in traditional notation:
p(x,y) := x³ + y²
In Prolog, the above function is modeled by a predicate e.g. poly/3
.
That predicate behaves as a function because two arguments have an “input” role and must be ground for evaluation to proceed (in other words, the mode of the logic variables is ++
). yall
, however, can work in more general settings.
Expressed with a procedure:
poly(X,Y,R) :- R is X*X*X + Y*Y.
?- poly(2,3,R).
R = 17.
We can perform indirect calls (not to be confused with call-by-name
) by constructing a term and handing it to call
. Again, this is not “calling a first-class function”, it’s just an indirect call:
?- call(poly(2,3,R)).
R = 17.
?- call(poly(2,3),R).
R = 17.
?- call(poly(2),3,R).
R = 17.
?- call(poly,2,3,R).
R = 17.
Same, but more flexibly, with apply
(variadic arguments? yes, please!):
?- apply(poly,[2,3,R]).
R = 17.
It is said that the above is slower than call/x
.
This can of course be done recursively, building up the apply
onion:
?- apply(apply(poly,[2,3,R]),[]).
R = 17.
Now we want to “wrap” the call to poly/3
to define a new function.
So, create a named shim poly_new/2
around poly/2
(with Xo
, Ro
standing for “X-outer”, “R-outer”). Note that in this example modification of values happens on “information inflow” and “information outflow”:
poly_new(Xo,Ro) :- X is Xo/2, poly(X,Xo,R), Ro is R*3.
?- poly_new(2,Ro).
Ro = 15.
This evidently computes:
poly_new_another(X,R) :- R is (X*X*X*3/8 + 3*X*X).
Using the yall
library, we can avoid having to explicitly define poly_new/2
predicate, writing the shim inline:
poly_new_yall(Xo,Ro) :-
call( [Xo,Ro] >> (X is Xo/2, poly(X,Xo,R), Ro is R*3), Xo, Ro ).
or using apply/2
:
poly2yall(Xo,Ro) :-
apply( [Xo,Ro] >> (X is Xo/2, poly(X,Xo,R), Ro is R*3), [Xo, Ro] ).
Note that the parentheses are important. What is inside apply
is a term, with the toplevel functor >>
.
Sadly (and maybe unexpectedly) one cannot write:
badpoly(Xo,Ro) :- ([Xo,Ro] >> (X is Xo/2, poly(X,Xo,R), Ro is R*3)).
The above compiles, but then:
?- badpoly(2,R).
ERROR: Domain error: `lambda_parameters' expected,
found `[2,_3938]>>(user:(_3956 is 2/2,poly(_3956,2,_3978),_3938 is _3978*3))'
The apply
-less syntax would demand native Prolog support. Until a new ISO Prolog standard has been written or Prolog diverges more from the standard, you have to call
or apply
around the >>
.
Again, this is just a shim, plumbing around poly
. There is no closure, nothing can be passed, nothing that is lexically scoped is retained for later:
The usefulness as a shorthand to avoid naming predicates becomes clear when you don’t use call
or apply
but use other predicates taking terms that can be completed and lifted to goals, like maplist
:
Instead of:
?- maplist(poly_new,[1,2,3,4,5],Out).
Out = [3.375, 15, 37.125, 72, 121.875].
write directly:
?- maplist(
[Xo,Ro] >> (X is Xo/2, poly(X,Xo,R), Ro is R*3),
[1,2,3,4,5],
Out).
Out = [3.375, 15, 37.125, 72, 121.875].