Combinator Module Users' Manual

Purpose

This document provides information on using the predicates exported from the combinator module. Furthermore, as the predicates, themselves, are derivative (they depend entirely on the syntax and semantics of the combinators and the language used to express them), this document provides an overview of the implementation language and the use of combinators.

Background

Combinatory logic (CL) is one language of mathematics.1 Like the lambda calculus, one expresses a function by combining a set of terms; unlike the lambda calculus, CL has no variables; the terms are combinators, not lambda terms, and the method of combining combinators is composition, not application, as it is for the lambda calculus. Given, or, despite, the similarities and differences of CL to the lambda calculus, the results of both languages is the same: one can build any arbitrary computable expression using a small set of "primitive" terms.

Concessions

Pure combinatory logic has normal order evaluation ("lazy"), and the result of composing combinators is a combinatory term. We allow some intrusions of the "real world" into this implementation of CL in order to simplify translating information between the rest of Prolog and the CL domain.

Syntax

We use the composition syntax as implemented by Unlambda:3 the grammar is unambiguous and is generally more succinct than other unambiguous grammars.4 The grammar is as follows:

<program> ::= (<composed-term> | <combinator>) <query-token>?
 

A combinatory logic program is a single combinatory expression. The program may be optionally decorated by the query token, which indicates that the program evaluates to a boolean expression that should be converted to the equivalent Prolog goal.

 
<expression> ::= <combinator-or-number> | <composed-term>
 

The expression is either a single combinator or a term composed of two or more combinators. The syntax of which is the following:

 
<composed-term> ::= <compose-token> <expression> <expression>
 

So, a composed-term is simply the composition of two expressions. The sub-expressions are evaluated, when necessary, before the entire term is evaluated.

 
<combinator-or-number> ::= <natural-number> | <operator> | <combinator>
 

The combinators are a subset of traditional combinators defined in [Smu1985]; the operators are the concessions made to "traditional" algebra for working with the natural numbers allowed in this language.

 
<combinator> ::= b | c | i | j | k | l | m | n | o | p | r | s | t | u | v | w
<operator> ::= < | > | = | + | * | - | / | :
 

The operators have their usual arithmetic meanings (the = combinator is the identity comparitor, not the assignment operator); the one special case is the : combinator which is taken to mean list-concatenation: 1:List prepends the element 1 to a List of elements.6

 
<natural-number> ::= <digit> | (<digit> <natural-number>)
<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
 

As we see above, combinators are selected letters of the alphabet, the meanings of which we will define below. The final bits of the syntax are the tokens:

 
<compose-token> ::= `
<query-token> ::= ?

Combinator Meanings

The combinators declared above each have an equivalent representation in the lambda calculus -- this gives them their respective definitions. The below table shows the combinators with their given names and their lambda-equivalents.

Combinator Given Name7 Lambda Equivalent
bBluebird Bxyz=x(yz)
cCardinal Cxyz=xzy
iIdentity Bird Ix=x
jJay Jxyzw=xy(xwz)
kKestrel Kxy=x
lLark Lxy=x(yy)
mMockingbird Mx=xx
n Nightingale8 Nx=xKSK
oOwl Oxy=y(xy)
p Penguin9 Pxyzwv=x(ywv)(zwv)
rRobin Rxyz=yzx
sStarling Sxyz=xz(yz)
tThrush Txy=yx
uTuring bird Uxy=y(xxy)
vVireo Vxyz=zxy
wWarbler Wxy=xyy

Examples

Subtract 2

The term "``c-2" subtracts 2 from a given number, so the goal

map("``c-2", [31, 41, 59, 26, 5], Ans)

Gives the list

Ans = [29, 39, 57, 24, 3]

If we were not to use combinators and the mapping predicate, the equivalent Prolog code to do the same transformation would be:

subtract2([Elt|Rest]) -->
  { NewElt is Elt - 2 },
  [NewElt],
  subtract2(Rest).
subtract2([]) --> [].

Increment

The term "`+1" increments a given number:

X := 1..10, map("`+1", X, Y)
=> Y = [2,3,4,5,6,7,8,9,10,11]

The equivalent combinator-free Prolog code:

inc([Elt|Rest]) -->
  { NewElt is Elt + 1 },
  [NewElt],
  inc(Rest).
inc([]) --> [].

Minimum Element

The term "```pt<v" is a predicate that returns true if the first given number is less than the second (the Vireo is how CL creates pairs). When applied over a list of numbers, it gives the minimum element of the list:

foldl([31,41,59,26,5], "```pt<v", 31, X)
=> X = 5

Prolog non-combinator equivalent:

min_list([Num|Nums], Accum, Ans) :-
  (Num < Accum -> NewAccum = Num; NewAccum = Accum),
  min_list(Nums, NewAccum, Ans).
min_list([]) --> [].

Sum of Squares

The term "``b+`w*" squares the given number and adds it to an accumulated result, essentially transforming an input list into the sum of the elements squared.

foldl([3, 4], "``b+`w*", 0, X)
=> X = 25

Prolog non-combinator equivalent:

sum_of_squares([Num|Nums], Accum, Ans) :-
  Square is Num * Num,
  NewAccum is Square + Accum,
  sum_of_squares(Nums, NewAccum, Ans).
sum_of_squares([]) --> [].

Summary

The above examples show that with map/3 and foldl/410 one may capture the programming logic in the combinator, and the recursion scheme falls out of the predicate selected: it is possible to write programs using combinators. Moreover these programs may be specified at any time: compiled into a module or even specified dynamically at runtime. Modules using combinators rely on a few simple predicates to accomplish this feat; below we review these predicates.

:- module(combinators)

Dependences

:- use_module(library(lambda), [apply/3, test/2]).

:- use_module(library(syntax), ['<-'/2]).

Exported Predicates

The combinators module exports the following predicates:

Exported Operators

This module does not export any operators.

compose/3


Synopsis: compose(+String, +Term, -Result)
Arguments:  

String

<string> or <atom> a combinator (atom) or combinator term (string)
Term<term> The argument to be composed with the given combinator
Result<term> The result of the composition of the combinator with the argument
Description: 

This predicate provides the fundamental operator of CL: composition. Note that it is entirely possible to compose two combinator expressions and to obtain a combinator as a result.

Backtracking: 

Standard.

Example: 

The primary user of compose/3 is the combinators module itself, as it uses this predicate to implement the "`" special form. Outside of this module, list_utils uses this predicate on occasion to construct a functional from partial or accumulating information. For example, one would code the following to find the set of numbers of a given list less than a supplied value:

all_less_than(Num, List, Ans) :-
  compose("``bw`c<", Num, LessThanNum),
  compose("?", LessThanNum, Filter),
  map(Filter, List, Ans).

all_less_than(5, [3,4,5,6,7], Ans) gives Ans = [3,4].

phi_equivalent/2


Synopsis: phi_equivalent(?Combinator, ?Phi)
Arguments:  

Combinator

<term> A combinator (atom), combinator term (string) or equivalent

Phi

<term> a phi-term of the form phi(+Arg, -Ans, +Goal)

where:

 
Arg<variable> Input argument captured as a variable, bound only during verification of Goal
Ans<variable> Output resulting from Goal applied to Arg
Goal<callable> Yields a proof resulting in Ans from a given Arg
Description: 

Converts the given input combinator (or equivalent, such as a lambda term) to a phi-equivalent. A phi-term is a generalization of a lambda-term in that it not only captures the input argument, but it also provides a method of obtaining the output result.

Backtracking: 

Standard. An unbound Combinator argument with a bound Phi one can lead to infinite regression.

Examples: 

The following shows the structure of the Identity bird

:- phi_equivalent(i, Phi), Phi = phi(X, X, true).

Lambda terms are also easily convertable:

phi_equivalent(lambda(X, X + 1), Phi)

phi_equivalent(Combinator, Phi) where both Combinator and Phi are free variables allows one to iterate, non-deterministically, through the declarations and definitions of the combinators of this module ... this call eventually falls into infinite regression.


Endnotes

1

There are many such languages: the lambda calculus is a closely related one, the language of set theory and the predicate and propositional calculi are others.

2

The y combinator is not implemented here for this reason of infinite regression, but it is also possible to construct it, quite easily, from other combinators implemented here. A simple construction: `uu.

3

Confusingly, Unlambda uses a set of combinators that have no basis in Combinatory Logic, so we will only use the syntax.

4

For most cases, Unlambda's syntax uses fewer constructs than, e.g., Smullyan's unambiguous grammar.5

5

As in chapter 25 of [Smu1985].

6

This follows the syntax of : in the Haskell programming language (http://haskell.org).

7

Smullyan gives combinators the names of birds in [Smu1985]; his practice has been universally adopted by the community.

8

The Nightingale is present because it is posed that there exists an unary-combinatory basis: a CL that can be expressed using only one combinator. It is therefore experimental in nature.

9

Further information on the Penguin is available at http://www.cotilliongroup.com/code/penguin.html.

10

Actually, only foldl/4 is necessary if element-order is not important, as the following query holds:

:- foldl(List, ":", [], Reversed),
   map(i, List, IdenticalList),
   IdenticalList = List,
   reverse(IdenticalList, Reversed).

Works Consulted

[Smu1985] To Mock a Mockingbird and Other Logic Puzzles Including an Amazing Adventure in Combinatory Logic, Raymond Smullyan, Alfred A. Knopf, NY, 1985.

author:Douglas M. Auclair (dauclair at hotmail dot com)
created:October 11, 2005
last modified:December 15, 2005
Copyright © 2005, Cotillion Group, Inc. All rights reserved.