Types in Concatenative Programming
A New Kind of Polymorphism
Types
As crazy as this looks, there is theory to support how concatenative languages work. How?
Recall: 2
is a function that takes no inputs and
returns one integer, itself. The more common *
is
a function that takes two integer inputs and returns one integer:
2 :: () → (int) 3 :: () → (int) * :: (int, int) → (int)
But, as written, we cannot compose these functions. The range of
2
does not match the domain of 3
.
The range of 2 · 3
, whatever that is, does
not match the domain of *
.
The basic idea is to give every function a generic data type that accounts for the full stack made available to it. Each function can be thought of as taking any input stack, as long as it is "topped" with the values it actually needs. The function then returns the arguments it doesn't use back to the stack, followed by its actual return values, which are pushed onto the top of the stack.
2 :: (A) → (A, int) 3 :: (B) → (B, int) * :: (C, int, int) → (C, int)
Now we can match B = (A, int) and compose 2 with 3:
2 3 :: (A) → (A, int, int)
This is quite nice! The meaning of 2 3
is clear.
It is a function that takes no input and returns both
2 and 3. Joy thus has functions that return multiple values!
Then we match C = A
in the result and compose
2 3
with *
:
2 3 :: (A) → (A, int, int) * :: (C, int, int) → (C, int) --------------------------------- 2 3 * :: (A) → (A, int)
It works! The program 2 3 *
takes no inputs from
the stack and produces one integer. Note, too, that this is the
same as the type of 6
, which is the value computed
by 2 3 *
.
6 :: (A) → (A, int)
This new approach to typing, called row polymorphism, does just what we need. Thanks to row polymorphism, we have a uniform way to compose functions of different types, in a way that makes the flow of data through a program clear. As a bonus, concatenative languages are able to give us something that applicative functional languages usually don't: functions that return true multiple values.