This article describes a new theory of combinators, similar in many ways to the theory of Combinatory Logic pioneered by Haskell Curry and others in the 1930s. Thus, although not essential, it would be helpful for the reader to have an understanding of the classical theory of combinators (see the links at the bottom of this article for some introductory material to combinators).
However, this theory of concatenative combinators is by no means just a reformalization of the classical theory of combinators; eventually, it will be clear that, in essence, the concatenative theory extends the classical theory by permitting multiple return values. This change adds no power to the system, because multiple return values can be simulated by a classical system using tuples, or lists. Nevertheless, this change causes some beautiful results, which will shortly be seen.
The inspiration for this theory came from the programming language Joy, designed by Manfred von Thun. It would be very helpful if the reader is basically familiar with Joy. In Joy, data is manipulated through a stack (there are no variables); in this way, it is similar to the classic programming language FORTH. However, Joy goes one step further and permits (and actively encourages) pushing programs themselves onto the stack, which can then be manipulated just like ordinary data.
In fact, the theory here is basically a subset of Joy in which programs are the only kind of data (i.e., numbers, string literals, and other kinds of data are not part of the theory here). To someone unfamiliar with combinatory logic, it might seem that no useful computations could be done without numbers, but it will soon be seen that numeric data can be simulated using concatenative combinators, just as they could using classical combinators.
I should make one disclaimer at this point: although the theory here is inspired by Joy, many of the combinators I'll use here are not included as primitives in the standard Joy distribution, or in some cases the names are different.
Also, note the approach here will be informal; it seems best to introduce the
concatenative theory in a gentle fashion since it is is not yet well known.
"swap" simply swaps the top two items on the stack, reversing their order.
"dup" just duplicates the top item, leaving a copy. "zap" erases the top item.
Later it will be seen that "swap", "dup", "zap" have a special relation to the
classic "C", "W", and "K" combinators, respectively.
Next, take a look at the combinators "cat", "cons", and "unit":
These combinators are special in that they restructure quotations. "cat"
takes two quotations and concatenates them together. "cons" does the same,
except that the bottom parameter is kept separately quoted inside the result;
another way of thinking of "cons" is that it takes a program "A", and gives back
what is still essentially the program "A", except that a parameter has been
hardwired into it (later, it will be seen that "cons" has a special relation to
the classic "B" combinator). "unit" takes a program "A" and leaves a program
that, when executed, leaves the program "A" on the stack; also, you could think
of "unit" as simply wrapping a quotation in a second layer.
Finally, take a look at the combinators "i" and "dip":
These combinators are special in that they dequote stack items, removing
their wrapping; in other words, these combinators execute programs on the stack.
The "i" combinator simply executes the top item on the stack. The "dip"
combinator executes the top item "A", but first it gets rid of the second item,
which is restored after the execution of "A" is complete. The "dip" combinator
will prove to be very versatile and also quite fundamental.
To give an idea of how constructions can happen, it should be pointed out
that the eight combinators above are not independent; many of them can be
defined in terms of each other. For example, take a look at the three
combinators "cat", "cons", and "unit" (these are the ones which had a
restructuring effect). It is possible to construct "cat" and "unit" in terms of
"cons":
This construction of "unit" is quite simple and elegant. Notice that it
employs the empty quotation "[]"; an empty quotation is in principle just like
any other quotation, except that its body is empty. In constructing simpler
combinators in terms of more complex ones (as we are here with "unit" in terms
of "cons"), we will often find "[]" to be useful.
The construction of "cat" above needs a bit of comment. Essentially, "[i] dip
i" is a program that runs the top two programs, in turn; the bottom one is run
first with "[i] dip", and then the top is run with "i". Of course, "[i] dip i"
itself is not a valid construction of "cat", since "cat" should not run the top
two programs, but should instead leave a program on the stack that would run
those two programs. This is where "cons" comes in. In all, this is how the
construction works:
This construction has a bit of a caveat, which should be pointed out: in
order for this construction to work out, we had apply rewrite rules inside of a
quotation. If a particular formalism considers this to be valid (and normally it
does), then we say that it has transparent quotation, whereas if this is
not valid, then it has opaque quotation. The standard Joy interpreter
actually has opaque quotation in that "[[B] [A] [i] dip i]" is considered
distinct from "[B A]", even though both quotations would do the same thing when
executed (they are distinct because, for example, "size" would give "5" in the
first case and perhaps "2" in the second). However, even though the two above
quotations can be distinguished, in most contexts they would be considered
interchangable.
So, we just showed how "cons" could be used to construct "unit" and "cat".
However, the reverse is also possible:
This constructs "cons" as basically "cat", except that the bottom parameter
is wrapped up first by "unit" (the "dip" is used so that the "unit" applies to
the bottom parameter rather than the top one).
Now, moving on to another construction, notice that "dip" and "swap" are
quite similar, in a way; the only difference is that "dip" executes the top
program, whereas "swap" leaves it quoted. This suggests the construction of
swap:
It is also possible to construct "dip" in terms of "swap":
It works like this:
Thus, "swap unit cat i", given two stack items, does the same thing that
"dip" does with them.
Finally, we'll give a curious way in which "i" can be constructed from "dip":
Here we are again constructing a simple primitive using more complex ones;
usually this type of construction is considered unelegant, and this one is
particularly bad in it relies on making an unnecessary copy (i.e., "dup" is used
to make a copy that is subsequently thrown away, unused, by "zap"). The
construction can perhaps be improved in this way:
This is the same as the last construction except that at the very end we use
"dip" to get rid of our useless "[]"; this works because "dip" just executes
"[]", which has no effect except to get rid of the "[]". The caveat of this
construction is that the final "dip" relies on there being an item to dip under;
it doesn't matter what the item is, but there needs to be one there, otherwise
an error may be generated. To put things in classical terms, a formalism like
the "eta-theory" (one with a principle of extensionality) would consider "[]
dip" to always be a valid program that does nothing, whereas the "beta-theory"
would not. In essense, the principle of extensionality corresponds to the
assumption that we have an infinite number of stack items at our disposal, if we
need them (which, in interpretation, is not necessarily valid).
to mean "pop the top item off the stack and store it in the 'A' variable".
Then, subsequent use of the letter "A" will result in the execution of the
program that was stored in it. For example, one could do
This would pop the top two items off the stack and call them "A" and "B";
then, "A" (the used-to-be top item of the stack) is executed, and then "B" is
pushed back on the stack. Note that this is what "dip" does, and in the fact the
above expression is a construction of "dip" using lambdas.
At this point, one might object that the above expression is not quite a
proper construction of "dip", because it has the side effect of leaving the
variables "A" and "B" with values still stored in them. This could be remedied
by adding a construct to end the scope of a variable (i.e., to erase the value
in it). For example, suppose we defined "A$" to end the scope of "A"; then the
proper construction of dip would be:
However, since there a variety of ways to formalize scoping, and since the
lack of scoping won't pose much of a problem in this article, we won't really
bother with it.
Now that we've defined the lambda construct, we can give a more precise
definition of what a combinator is. A combinator is a program that can be
expressed as a series of lambdas, followed by a concatenative expression purely
in terms of the variables used in those lambdas; for example, these are all
combinators:
The second combinator may seem a little exotic, since it uses an empty
quotation (the last combinator listed, "[]", is even more so exotic, since it
takes no parameters); however, we see no reason to exclude it. Even the empty
program itself, "", qualifies as a combinator (although it's a rather trivial
one). Note, the third combinator uses neither of its parameters, which is
acceptable; at least, it is acceptable in the sense that it still qualifies as
being a combinator; however, there is interest in a theory which, like the
"I-calculus" of classical combinators, excludes combinators that ignore one or
more of their parameters; we will consider this in the section on "Conservative
Completeness".
Since we are relying mainly on examples to define what is meant by
"combinators", it is best that we also give some examples of what are not
considered combinators:
These fail to be combinators because their right-hand-sides (i.e., the part
after the lambdas) fail to be purely in terms of the variables. In the first
example, the right-hand side contains "dip", which is not acceptable, even
though "dip" could itself be expressed in terms of lambdas. In the second
example, a lambda is used in the right-hand side, which is not acceptable. In
the third example, "i" is used on the right hand side (the whole thing is the
right-hand side in this case, since there are no lambdas at the beginning).
Even though the above programs fail to be combinators, we will award them the
title pseudo-combinators. A pseudo-combinator is any non-combinator that
can be constructed purely from lambdas (or purely from other combinators). The
family of combinators and pseudo-combinators together we will call pure
programs.
Because we feel that the reader could be bored or confused by a precise
statement of the algorithm, we'll define it by just giving an example of how it
works. Suppose we want to eliminate the lambda in this program:
This program begins by popping "A" off, and then executing "B" (which does
not use "A"). We could postpone the lambda by not immediately popping "A" off
the stack, but by instead using "dip" to make "B" look under it; here's a
reduction of the above program:
Note that this is progress toward eliminating the lambda, since we have
reduced its scope. At this point, we should note that "A", after it is popped
off, is used twice. Using "dup", we can reduce this lambda to two lambdas that
only use their variable once:
And now, "A2\ A2" can be replaced by just "i":
Next, we use "dip" again:
Now, we have the situation where "A1" is popped off and then a quotation is
pushed which uses it; we can reduce this using "cons":
Now the lambda is easily eliminated:
From this example, hopefully the reader can see the idea of how to eliminate
lambdas using {i, dip, cons, dup, zap}; we didn't need "zap" in this example,
but it would have been neccessary if we had been trying to eliminate a
vacuous lambda (i.e., a lambda in which the variable is never used).
You can think of "sip" as executing the top program "A" on the stack, except
that it first saves a copy of the next item "B" and restores it when "A" is
done.
In a way, "sip" resembles "dip" in the way that it runs a program and then
restores data; but also, "sip" has a bit of "dup" in it, because it uses an item
multiple times. In fact, "sip" can play the role of both "dip" and "dup" in the
sense that {i, cons, sip, zap} is a complete base. To show this, we only have to
demonstrate that "dip" and "dup" can be constructed from it; "dup" is rather
simple:
To construct "dip" is a bit trickier. First, we'll try to construct "[A] dip"
(a particular instance of "dip", so to speak), which is an easier task. Well,
"[A] sip" does essentially the same thing as "[A] dip" except that it leaves an
undesired copy of the next stack item ("B") for the program "A"; but we can
easily get rid of this extra copy by using "zap", in this way:
So, this gives us a way of constructing an instance of "dip", but it remains
to construct "dip" itself. We'll do by using a lambda:
Of course, this is not really a satisfactory construction of "dip"; we'd like
to get rid of the lambda. Fortunately, it is not difficult:
Now, this construction still has a problem in that "dip" occurs on the right
hand side, whereas we would like the right hand side to only contain members of
the base {i, cons, sip, zap}. Fortunately, in this case, the "dip" occurs as a
particular instance (i.e., a quotation immediately precedes the use of "dip"),
and we can get rid of it by using the fact that
or in this case, that
Using this, finally we get the construction of "dip":
As the reader can probably see, constructing combinators from a restricted
base is an art. One might wonder whether the above construction of "dip" is a
good one; that is, is there a shorter or more elegant one? Well, a computer
search reveals that in fact there is no shorter one, and only one of the same
length:
This is essentially the same construction except that "cons zap" is used as a
synonym for "zap zap", since both just destroy the top two stack items. More
information on the computer search tool will be given later in the article.
Note that "dig1" is the same as "swap".
Next we'll look at a complementary scheme of combinators, "buryn". This
simply takes the top item and buries it under "n" items:
Note that "bury1" is also "swap".
Now, we'll look at how to construct these combinators. Rather than bore the
reader with the details leading up to this, we'll just give the results. The
most straightforward scheme for constructing "dign" is thus:
In essence, how this works is that we wrap the top few stack items up with
"[] cons cons ...", until all items that we are dealing with are wrapped up,
except one (the item we what to be dug up); then, using "dip", we simultaneously
bury and unwrap the whole package. For example, here is how "dig3" works:
Now we'll look at how to construct "buryn". Interestingly, "buryn" is more
difficult to construct than "dign". The simplest general way, perhaps, is thus:
Like our construction of "dign", this works by wrapping up all the items that
we want to get out of the way, and then dealing with them all at once, in their
bundled form. Here is how "bury3" works:
Of course, these constructions of "dign" and "buryn" are not the only ones;
here are some alternatives:
When we try to construct "flipn", we find that it is very useful to first
define the following combinator:
You can look at "take" as taking in a parameter into a quotation, similar to
"cons", except that "take" takes the parameter into the end of the quotation,
instead of the beginning. Another way to look at "take" is that it is kind of a
postponed "dip"; it is like "dip", except that the results are left enclosed in
a quotation. In fact, by using "i" to unwrap this quotation, we can use "take"
to construct "dip":
Similarly, we could define "take" in terms of "dip":
As it turns out, "take" is a very versatile combinator in its own right, but
it is especially useful in constructing "flipn":
It works like this:
Essentially we're wrapping the whole thing up, in reverse order, using
"take", and then we unwrap the whole thing at the end using "i". By the way, we
can compact our construction of "flipn" a bit by using the fact that "[] take"
is the same as "unit" and "take i" is the same as "dip":
Now, there is a fascinating connection among the three schemes "dign",
"buryn", and "flipn": Any one of them can be defined elegantly in terms of one
of the others (we are using case 4 as an example; hopefully the reader can see
how these generalize to any case):
We'll leave it to the reader to examine the details of how this works.
Note that "peek0" acts simply as "dup". Also, "poke0" acts as "[zap] dip",
also known as "NIP" from FORTH. Also, "peek1" is known as "OVER" in FORTH, and
for completeness we will refer to "poke1" as "under":
The reader may wonder at our choice of numbering, why it is that a "peek2" is
numbered "2" when it takes three parameters, and why "poke2" is called such when
it takes four parameters. The reason is that, in both cases, "2" is the number
of items that are "dipped under" and are unmoved (and not copied or destroyed).
It should also be noted that with this numbering, "poke2" is an inverse to
"peek2" (i.e., in undos its effect).
Now, when we try to construct "peekn", we get a very pleasant result, similar
to "dign":
This is exactly the same as the construction of "dign", except that now we
use "sip" at the end, instead of "dip".
However, as might be expected, "poken" requires a bit more complex of a
construction:
This is exactly the same as the construction of "buryn", except that now we
use an extra "nip".
Once again, there are alternative constructions; here is another possibility:
We'd like to introduce extensions to the "dip" and "sip" combinators which
turn out to be useful. First, "dipn":
These extensions are like "dip", except that they can dip under more than
item; for example, "dip2" dips under two items. Of course, it is always the top
item "A" which is dequoted.
The construction of "dipn" is very similar to the construction for "buryn";
here are several possible ways:
Next we'll introduce "sipn":
These extensions are like "sip" except that they save more than one item, and
restore them after "A" is executed. Here is a straightforward way of
constructing "sipn".
Here we're using a combinator "swat" as a contraction of "swap cat"; think of
"swat" as prepending something to a program. And, here is a curious alternative
construction of "sipn":
The "w" combinator is similar to "dup", except that "w" uses an extra
parameter "A" which is dequoted after the duplication. A similar relation exists
between "k"/"zap", "b"/"cons", and "c"/swap. Here's a way to state it precisely:
This expresses "w", for example, as doing a dipped "dup", and then dequoting
the extra "A" parameter with "i". The constructions can easily go the other way,
as well, though:
A bit of study reveals that the whole classical theory of combinators can be
elegantly nestled within the concatenative theory. For example, recall the
classical construction:
Well, in the concatenative system, the very same construction exists:
Perhaps the similarity is easier to see if we write the original classical
construction using a more strict scheme of parentheses, where every application
is written in the form "F(X)":
It should now be clear the concatenative construction is exactly identical,
except the entire expression is reversed, and "()"s are replaced by "[]"s.
It is hard to pinpoint in informal terms why this connection exists; however,
we can elaborate a bit on the requirement that a combinator must have in order
to participate in it: the right-hand-side of a combinators rewrite rule must
consist of a single dequoted term (e.g., "A", or "B"), optionally preceded by
one or more quotations whose bodies follow the same rule (i.e., the body of each
quotation must consist of a single dequoted term, optionally preceded by such
other quotations).
Here's another way of saying it: to participate, the right-hand-side of a
combinators rewrite rule must be an applicative expression. We
recursively define an applicative expression to be either be an atom ("A", "B",
etc.) or an expression of the form "[X] Y" where both "X" and "Y" are
applicative expressions.
So, it can be seen that each of the combinators "w", "k", "b", "c", and "s"
(and also "i") follows this requirement. However, note that most combinators
we've discussed do not, such as "dup", "zap", "cons", or "dip"; none of these
combinators contain the required single dequoted atom at the end of their
right-hand-side.
Finally, we would like to introduce one more applicative combinator:
We use this combinator to make the point that an applicative combinator need
not always end by dequoting the top stack item, as "i", "w", "k", "b", "c", and
"s" do; in this case, it ends by dequoting "B" (the second stack item). However,
"t" is an interesting combinator in its own right.
But first, we'd like to examine the base {cons, sip, k}. This is a natural
reduction of {i, cons, sip, zap}, relying on the "k" combinator to perform both
the dequoting job of "i" and the destructive job of "zap". Recall that
Thus, "k" executes the top program, but before it does that, it destroys the
stack item beneath. We will find that "k" is often very useful in our search for
a minimal base.
To show that {cons, sip, k} is complete, it is sufficient to show that "i"
and "zap" can be constructed from it. This can be done without much difficulty:
This "zap" works by pushing an empty quotation, which allow "k" to destroy
the original first item (because "k" can only destroy the second item down);
then the empty quotation is executed, which does nothing.
The "i" works by making a copy of the top item with "[] sip", and then
simultaneously executing one copy and discarding the other one, using "k".
Now we'd like to introduce a complete two-combinator base and give some
explanation for how it came about. First, we know that the two combinators "s"
and "k" are very powerful:
From them it is possible to form all applicative combinators, including "i",
"b", "c", and "w". And from those we know it is easy to form "cons", "swap", and
"dup" (also, from "k" we can easily form "zap"). This almost gives us
completeness; however, there is no way to form "dip" or "sip" using just "s" and
"k" because, roughly speaking, they provide no way to dequote items buried in
the stack.
However, we can make progress if we use a slightly modified "s":
This "s'" is like "s", except that it takes an extra parameter "X" on top and
dequotes it in the middle of the action (which, hopefully, should allow us to
form "dip"). We picked the design of "s'" so that we could easily get "s" back
out of it:
Now, here is a way to construct "dip":
It works like this:
Alternatively, here is how to construct several of the basic combinators
(including dip) straight from the primitives "s'" and "k":
The reader may agree that "s'" is an ugly combinator to deal with in the
base; however, it is interesting that it works.
Now we will describe a base that is slightly more elegant than {s', k}. The
guest of honor will be a combinator that we'll call "mud" (in honor of its
complex and seemingly meaningless transformation of the stack):
Note, "mud" takes only three parameters, compared to the four parameters of
"s'". So, in that respect, it seems better as a base. Here is how some basic
combinators can be constructed from {mud, k}:
The name "cake" is a contraction of "cons" and "take", which describes what
this combinator does; we can express it more precisely this way:
Note that "cake" takes only two parameters, and that its right-hand-side has
symmetry. The way that the basic combinators flow from "cake" is quite nice,
also:
The question arises, is there a more simple conservative base than {i, cons,
dip, dup}. As it turns out, there is a conservative two-combinator base;
however, it is very ugly. It features the 5-parametered combinator "j'":
The original "J" combinator of the classical theory is available simply as
"[] j'", and has this function:
Now, it is known that in the classical theory, "J" and "I" suffice to give
conservative completeness; thus, from "j" and "i" it is possible to construct
"w" and "b" and thus "dup" and "cons". That would give us "i", "dup", and
"cons". The only other thing we need is "dip":
Now, the reader may be curious for some more details as to how things can be
constructed:
It is unknown whether there is a more elegant conservative two-combinator
base.
The most obvious linear base is {i, cons, dip}; however, one interesting
alternative is {take, cat, i}. This base may be interesting because with "take"
and "i" alone it is possible to reshuffle the stack in any way (remember that
"flipn", and thus "buryn" and "dign" are constructible from "take" and "i");
moreover, this can be done without using any quotations except for the empty
quotation "[]". For example:
Some other basic constructions in {take, cat, i} are as follows:
Now, we'll turn our attention to a two-combinator linear base, {cons, sap},
where
"sap" is quite a nice, simple combinator which simultaneously dequotes two
items, in reverse order; it may be expressed "dip i". Now, in {cons, sap}, we
can construct "i" and "dip" in this way:
This gives us "i", "dip", and "cons", which is sufficient for linear
completeness.
To construct these, it is handy to define a combinator "run" as follows:
Then, "repn" can be constructed thus:
To compact things, we can use the fact that "run zap == i":
Finally, we'd like to introduce "zn", a series of applicative combinators:
The interesting thing about these combinators is that they can be used to
represent numbers, by defining the operations of arithmetic (multiplication,
addition, and exponentiation) in this way:
For example, it will be found that:
These combinators are called the Church numerals, in honor of Alonzo
Church, who first put them forth.
If a similar attempt is made to try to use "repn" to represent numbers, we'll
find that the appropriate arithmetic operators are as follows:
However, in a way, this is less elegant because the above "^" operator is
only a pseudo-combinator, whereas all three operators for "zn" were true
combinators.
Before leaving the topic of iterators, one possible theoretical application
of them should be mentioned. As defined above, both the "zn" and "repn"
combinators are only capable of representing whole numbers. However, consider
what the implications would be if somehow one could define a "rep-1" (negative
one) primitive. This primitive would have to "undo" a program. But, if such a
primitive could be implemented, perhaps in a limited context, then it possible
to represent rational numbers, and even complex numbers, using the extended
"repn" or "zn" combinators; for example:
This defines the "0.5" case of "repn", which would execute a program "half" a
time, if that is possible. Using that, then, we can define the square root of
negative one, an imaginary number, as follows:
If a system could be formulated implementing this, then perhaps this could
serve as a new approach to the foundations of mathematics.
As a final note, it might be thought that executing a program "half" a time
is pure nonsense; however, for many programs it may be meaningful. For example,
say you have a program that adds "10" to a number; naturally, to execute that
program half a time, you would simply add "5". From a fresh perspective, any
negative or fractional parts in mathematics may seem like nonsense; how can one
have a negative number of apples, or a fractional number of siblings? But yet, a
fractional amount of a pie, or a negative amount of money (debt) makes sense. It
seems that anytime one is dealing with fractional or negative quantities,
regardless of whether they are implemented with combinators, they are only going
to make sense in certain contexts.
Unfortunately, the program relies on a poor algorithm, which limits its use
to finding constructions of only very small size. Essentially, the program works
by making a particular program of size 1 (i.e., only one word long, where a
quotation counts as a single word), and then giving it a test run to see if it
generates the desired effect on the stack; then, the program moves onto the next
program of size 1, and tests it, and so forth, until they all programs of size 1
been exhausted, at which point the program moves on to size 2.
A dramatic improvement could result if the program would incrementally try
the candidate program, and backtrack when an error occured. An error would
occur, for example, when executing "dip" and only one stack item is available;
also, it is safe to backtrack if a primitive is dequoted that does not match up
with the goal; e.g., if the goal is "0[2][1]", and somewhere along the way, the
candidate program runs "1", then it is not possible that this candidate program
could ever match the goal, since the goal requires the program to begin by
executing "0"; thus, by backtracking, huge masses of programs may be cut out of
the search space.
A further suggestion is to postpone filling in quotations until after their
context is clear. In other words, actually have the body of quotations stored at
the end of the expression. The reason for this is that, when we're about to push
a quotation, any quotation seems valid, and so there's a huge search space; but,
if we wait until later when the quotation is executed, there will be much fewer
possible valid things that the quotation could be.
Compared to the classical theory of combinators, the concatenative one has
the attractive property that we are easily able to make do with mainly
one-parametered and two-parametered words (e.g., i, dup, zap, dip, cons),
whereas in the classical theory we tend to rely heavily on three-parametered
words as well (e.g., B, C, S). However, the concatenative theory is slightly
more complex syntactically, because there are two constructs (quotation and
concatenation), whereas in the classical theory there is only one construct
(unary function application). The two theories seem very similar overall, yet
they contrast in how the details work out.
Baker, Henry. Linear Logic and Permutation Stacks -- The Forth Shall Be
First. http://home.pipeline.com/~hbaker1/ForthStack.html.
Barker, Chris. Iota and Jot: the simplest languages? http://ling.ucsd.edu/~barker/Iota/.
Curry, Haskell and Robert Feys. Combinatory Logic. North-Holland:
Amsterdam, 1958.
Keenan, David. To Dissect a Mockingbird. http://uq.net.au/~zzdkeena/Lambda/index.htm.
Smullyan, Raymond. To Mock a Mockingbird. Alfred A. Knopf: New York,
1985.
Thun, Manfred. The main page for the programming language Joy. http://www.latrobe.edu.au/philosophy/phimvt/joy.html.
A Few Combinators: swap, dup, zap, unit, cat, cons, i, dip
Now, we'd
like to introduce some fundamental combinators. Later in the article, we'll more
precisely define what we mean by "combinator", but for now, roughly speaking, a
combinator is a program that duplicates, destroys, rearranges, or restructures
items on the stack. Perhaps the most famous combinators are "swap", "dup", and
"zap" (aka "pop" or "drop"). We can express the behavior of these combinators by
giving a rewrite rule for each one:
[B] [A] swap == [A] [B]
[A] dup == [A] [A]
[A] zap ==
[B] [A] cat == [B A]
[B] [A] cons == [[B] A]
[A] unit == [[A]]
[A] i == A
[B] [A] dip == A [B]
Interconstructability of Combinators
The eight combinators presented
above are by no means all of the combinators; there are infinitely many
combinators. However, eventually we'll show that from the above combinators, it
is possible to construct all other combinators. In fact, it will turn out that
there is a base consisting of just two combinators, from which all other
combinators can be constructed.
cat == [[i] dip i] cons cons
unit == [] cons
[B] [A] [[i] dip i] cons cons
== [B] [[A] [i] dip i] cons
== [[B] [A] [i] dip i]
== [[B] i [A] i]
== [B A]
cons == [unit] dip cat
swap == unit dip
dip == swap unit cat i
[B] [A] swap unit cat i
== [A] [B] unit cat i
== [A] [[B]] cat i
== [A [B]] i
== A [B]
i == dup dip zap
i == [[]] dip dip zap
Here we insert a "[]" under the top item, using "[[]] dip" (equivalently,
we could have done "[] swap"). The "[]" serves as the useless item that we dip
under, and is subsequently destroyed by "zap". In some contexts we could even
use this construction:
i == [[]] dip dip dip
Lambdas
Now we'd like to introduce a construct that will turn out to be
analagous to the lambda of the classical lambda calculus. We'll use the
expression
A\
A\ B\ A [B]
A\ B\ A [B] B$ A$
A\ B\ C\ C [[B C] C] A
A\ A [] A
A\ B\
[]
A\ B\ [[A] dip] B
A\ B\ A B [C\ A]
[i]
An Abstraction Algorithm
Now, we'll show that it is possible to
eliminate lambdas using combinators; more specifically, using just the
combinators "i", "dip", "cons", "dup", and "zap", it is possible to rewrite any
expression containing lambdas to an expression not containing lambdas. A
consequence of this is that all combinators (and pseudo-combinators) can be
constructed from just "i", "dip", "cons", "dup", "zap"; in other words, we'll
say that {i, dip, cons, dup, zap} is a complete base.
A\ B A [C A]
[B] dip A\ A [C A]
[B] dip dup A1\ A2\ A2 [C A1]
[B] dip dup A1\ i [C A1]
[B] dip dup [i] dip A1\ [C A1]
[B] dip dup [i] dip [A1\ C A1] cons
[B] dip dup [i] dip [[C] dip A1\ A1] cons
[B] dip dup [i] dip [[C] dip i] cons
The sip Combinator
A moment ago, we showed that {i, dip, cons, dup, zap}
is a complete base. Now, those five combinators are by no means the only ones
that can be chosen to form a base. Much of the remainder of this article will be
exploring the other possibilities for bases. Right now, we'd like to introduce a
very versatile combinator called "sip" and show how it can be used in a base:
[B] [A] sip == [B] A [B]
dup == [] sip
[A] dip == [zap A] sip
dip == A\ [zap A] sip
dip == A\ [zap A] sip
== [A\ zap A] cons sip
== [[zap] dip A\ A] cons sip
== [[zap] dip i] cons sip
[A] dip == [zap A] sip
[zap] dip == [zap zap] sip
dip == [[zap zap] sip i] cons sip
dip == [[cons zap] sip i] cons sip
Schemes of Combinators: dign, buryn
Now we'd like to introduce some
schemes of combinators (i.e., sequences of combinators that serve a similar
function). First, we'd like to introduce "dign"; this is a combinator that
reaches under "n" items and "digs" up the next item all the way to the top. We
can most clearly define it with an example:
[C] [B] [A] dig2 == [B] [A] [C]
[C] [B] [A] bury2 == [A] [C] [B]
dig1 == [] cons dip
dig2 == [] cons cons dip
dig3 == [] cons cons cons dip
(...)
[D] [C] [B] [A] [] cons cons cons dip
== [D] [C] [B] [[A]] cons cons dip
== [D] [C] [[B] [A]] cons dip
== [D] [[C] [B] [A]] dip
== [C] [B] [A] [D]
bury1 == [[] cons] dip swap i
bury2 == [[] cons cons] dip swap i
bury3 == [[] cons cons cons] dip swap i
[D] [C] [B] [A] [[] cons cons cons] dip swap i
== [D] [C] [B] [] cons cons cons [A] swap i
== [D] [C] [[B]] cons cons [A] swap i
== [D] [[C] [B]] cons [A] swap i
== [[D] [C] [B]] [A] swap i
== [A] [[D] [C] [B]] i
== [A] [D] [C] [B]
bury4 == swap [swap [swap [swap] dip] dip] dip
== [[[swap] cons dip] cons dip] cons dip
== [swap] cons [dip] cons [dip] cons dip
dig4 == [[[swap] dip swap] dip swap] dip swap
Another Combinator Scheme: flipn
Now we'd like to introduce a very
special scheme of combinators, "flipn", which simply reverses the order of the
top "n" items; for example,
[C] [B] [A] flip3 == [A] [B] [C]
[B] [A] take == [A [B]]
dip == take i
take == [dip] cons cons
flip2 == [] take take i
flip3 == [] take take take i
flip4 == [] take take take take i
(...)
[C] [B] [A] [] take take take i
[C] [B] [[A]] take take i
[C] [[A] [B]] take i
[[A] [B] [C]] i
[A] [B] [C]
flip4 == [] take take take take i
== unit take take dip
dig4 == bury4 bury4 bury4 bury4
== flip4 flip5
bury4 == dig4 dig4 dig4 dig4
== flip5 flip4
flip4 == bury3 bury2 bury1
== dig1 dig2 dig3
More Schemes: peekn, poken
Now we'd like to look at a pair of
complementary schemes, "peekn" and "poken"; these are similar to "dig" and
"bury", respectively. However, unlike "dig", "peekn" only digs up a copy (i.e.,
the original also remains in its place), and unlike "bury", "poken" destroys the
object in the place of one being buried:
[C] [B] [A] peek2 == [C] [B] [A] [C]
[D] [C] [B] [A] poke2 == [A] [C] [B]
[B] [A] nip == [A]
[B] [A] over == [B] [A] [B]
[C] [B] [A] under == [A] [B]
peek0 == [] sip
peek1 == [] cons sip
peek2 == [] cons cons sip
(...)
poke0 == [[] nip] dip swap i
poke1 == [[] cons nip] dip swap i
poke2 == [[] cons cons nip] dip swap i
peek4 == [[[[dup] dip swap] dip swap] dip swap] dip swap
poke4 == swap [swap [swap [swap [nip] dip] dip] dip] dip
Two More Schemes: dipn, sipn
[A] dip0 == A
[B] [A] dip1 == A [B]
[C] [B] [A] dip2 == A [C] [B]
[D] [C] [B] [A] dip3 == A [D] [C] [B]
dip4 == [unit cons cons cons] dip dip i
== swap [swap [swap [dip] dip] dip] dip
== [[[dip] cons dip] cons dip] cons dip
== [dip] cons [dip] cons [dip] cons dip
[A] sip0 == A
[B] [A] sip1 == [B] A [B]
[C] [B] [A] sip2 == [C] [B] A [C] [B]
[D] [C] [B] [A] sip3 == [D] [C] [B] A [D] [C] [B]
sip4 == [unit cons cons cons] dip [i] swat sip i
sip4 == [cons cons cons sip] dup dup dup i
Applicative Combinators: w, k, b, c, s
We'd now like to define several
new combinators related to the classical theory of combinators:
[B] [A] w == [B] [B] A
[B] [A] k == A
[C] [B] [A] b == [[C] B] A
[C] [B] [A] c == [B] [C] A
[C] [B] [A] s == [[C] B] [C] A
w == [dup] dip i
k == [zap] dip i
b == [cons] dip i
c == [swap] dip i
dup == [] w
zap == [] k
cons == [] b
swap == [] c
B == S(KS)K
b == [k] [[s] k] s
B == S(K(S))(K)
[B] [A] t == [A] B
Towards a Minimal Base: {cons, sip, k}
The smallest complete base we've
mentioned so far is {i, cons, sip, zap}. It is well known that in classical
combinatory logic there is a complete two-combinator base {S, K}. It might be
wondered if there is a similar two-combinator base in the concatenative theory.
Later, we will see that there is.
[B] [A] k == A
zap == [] k
i == [] sip k
A Smaller Base: {s', k}
[B] [A] k == A
[C] [B] [A] s == [[C] B] [C] A
[C] [B] [A] [X] s' == [[C] B] X [C] A
s == [] s'
dip == [] swap [] swap [k] cons s'
[B] [A] [] swap [] swap [k] cons s'
== [B] [] [A] [] swap [k] cons s'
== [B] [] [] [A] [k] cons s'
== [B] [] [] [[A] k] s'
== [[B]] [A] k [B]
== A [B]
i == [] [k] [] s'
cons == [[] k] [] s'
dip == [k] [[] [[]]] [] s' s' s'
sip == [k] [] [[] [[]] s' k] s'
dup == [[] [k] [] s'] [] [] s'
zap == [] k
A Simpler Base: {mud, k}
[C] [B] [A] mud == A [[C] [C] B]
zap == [] k
i == [k] [[]] mud k
cons == [zap k] [] mud [] mud
dip == [k] [[k]] mud mud
dup == [] [[]] mud k
An Elegant Base: {cake, k}
Now, finally we will introduce a more
beautiful combinator, "cake", and show how it, with "k", forms a complete base:
[B] [A] cake == [[B] A] [A [B]]
cake == [cons] sip2 take
zap == [] k
dip == cake k
cons == cake [] k
i == [[]] dip k
dup == [] cake dip dip
Conservative Completeness: {j', i}
In the classical theory of
combinatory logic, there was some interest in systems where destructions were
not allowed. In the concatenative theory, that would be analagous to a system
based on, for example, the combinators {i, cons, dip, dup}, there being an
absense of "zap". We will refer to this type of system as conservative.
[D] [C] [B] [A] [X] j' == [[C] X [D] A] [B] A
[D] [C] [B] [A] j == [[C] [D] A] [B] A
dip == [] [] [i] j' i i
swap == [] [] j i i
cons == swap [] [i] j
t == [i] [i] j
dup == [[] [] []] dip [t t] cons j
Linear Completeness: {take, cat, i} and {cons, sap}
Now we'll examine
what happens if we take the conservative restriction one step further by
banishing combinators that have a duplicative effect (as well as those that a
destructive effect). A system that has both these restrictions we'll call
linear.
bury3 == [] take take take take i [] take take take i
dig3 == [] take take take i [] take take take take i
dip == take i
unit == [] take
cons == [unit] dip cat
swap == [] take take i
[B] [A] sap == A B
i == [] sap
dip == [] cons [[] cons] sap sap
Iterators: repn, zn
Finally, we'd like to introduce two more schemes of
combinators; first, "repn", which simply takes a program and executes it a
certain number of times.
[A] rep0 ==
[A] rep1 == A
[A] rep2 == A A
[A] rep3 == A A A
(...)
[A] run == A [A]
rep0 == zap
rep1 == run zap
rep2 == run run zap
rep3 == run run run zap
(...)
rep0 == zap
rep1 == i
rep2 == run i
rep3 == run run i
rep4 == run run run i
(...)
[B] [A] z0 == B
[B] [A] z1 == [B] A
[B] [A] z2 == [[B] A] A
[B] [A] z3 == [[[B] A] A] A
(...)
* == b
+ == [cons b] cons s
^ == i
z5 == [z3] [z2] +
z6 == [z3] [z2] *
z9 == [z3] [z2] ^
* == b
+ == [sip] sap
^ == [[cons] cons] sap i
half == [rep1] [[rep2] *] rep-1
== [rep2] [rep-1] ^
imag == [rep-1] [half] ^
A Brute-force Automated Construction Finder
There is a small program
available at http://tunes.org/~iepos/joys.zip
which will search for the smallest constructions of a particular combinator,
given a particular base. There is a README enclosed which explains the usage of
the program.
Conclusion
We've seen that the concatenative theory of combinators is
very rich indeed. To conclude, it seems appropriate to make a brief comparison
between the concatenative theory and the classical theory.
Appendix: Combinators
[A] zap ==
[A] i == A
[A] unit == [[A]]
[A] rep == A A
[A] m == [A] A
[A] run == A [A]
[A] dup == [A] [A]
[B] [A] k == A
[B] [A] z == B
[B] [A] nip == [A]
[B] [A] sap == A B
[B] [A] t == [A] B
[B] [A] dip == A [B]
[B] [A] cat == [B A]
[B] [A] swat == [A B]
[B] [A] swap == [A] [B]
[B] [A] cons == [[B] A]
[B] [A] take == [A [B]]
[B] [A] tack == [B [A]]
[B] [A] sip == [B] A [B]
[B] [A] w == [B] [B] A
[B] [A] peek == [B] [A] [B]
[B] [A] cake == [[B] A] [A [B]]
[C] [B] [A] poke == [A] [B]
[C] [B] [A] b == [[C] B] A
[C] [B] [A] c == [B] [C] A
[C] [B] [A] dig == [B] [A] [C]
[C] [B] [A] bury == [A] [C] [B]
[C] [B] [A] flip == [A] [B] [C]
[C] [B] [A] s == [[C] B] [C] A
[D] [C] [B] [A] s' == [[D] C] A [D] B
[D] [C] [B] [A] j == [[C] [D] A] [B] A
[E] [D] [C] [B] [A] j' == [[D] A [E] B] [C] B
References