Re: [stack] syntax preference question

John Nowak <[email protected]>
Newsgroups gmane.comp.lang.concatenative
Message-ID <[email protected]>
On Jun 17, 2008, at 8:03 PM, William Tanksley, Jr wrote:

> John Nowak <[email protected]> wrote:
>
>> Say I write this definition for a combining form:
>>   foo(a, b) = [a, b]
>> If cleave is type-directed, there's no way to give this a type. You'd
>> need to require an annotation in such a case.
>
> I don't get it. Can you provide a specific counterexample where this
> doesn't receive a type?

Ah, I knew I didn't explain that properly (well, at all really). Let  
me recap briefly so I can be sure I've covered it.

Factor has a family cleave combinators like bi, tri, etc, which I  
assume you're familiar with (otherwise this may not make any sense).  
Here is the type of 'bi' in 5th:

    bi(A b -> C, C b -> D) => A b -> D

What this says is that bi takes one function of type 'A b -> C' and  
one of type 'C b -> D' and yields a function of type 'A b -> D'.  
(Remember that 'bi' is a combining form (aka functional) in 5th, so it  
takes functions as arguments and yields a function as a result.) The  
type of 'tri' is just an extension of the type of 'bi':

    tri(A b -> C, C b -> D, D b -> E) => A b -> E

Factor has a combinator named 'cleave' that generalizes these. For  
example, instead of writing '[ a ] [ b ] [ c ] tri', you can write  
'{ [ a ] [ b ] [ c ] } cleave'. 5th can handle cleave properly. To be  
clear on the translation:

    bi(a, b)      =  cleave(a, b)     =  keep(a) b
    tri(a, b, c)  =  cleave(a, b, c)  =  keep(a) keep(b) c

Factor also has the combinators '2bi' and '2tri'. These save and push  
on the top two elements for each quotation rather than just one. The  
types are as follows (forgive me if I'm stating the obvious):

    2bi(A b c -> D, D b c -> E) => A b c -> E
    2tri(A b c -> D, D b c -> E, E b c -> F) => A b c -> F

And the equalities:

    2bi(a, b)      =  2cleave(a, b)     =  2keep(a) b
    2tri(a, b, c)  =  2cleave(a, b, c)  =  2keep(a) 2keep(b) c

Now, for a brief digression. Factor also has 'bi@' 'tri@' combinators.  
Appending the "@" means that the same function is used for all cases:

    bi@(a)   =  cleave(a, a)     =  keep(a) a
    tri@(a)  =  cleave(a, a, a)  =  keep(a) (a) a

To type these properly, we need to use intersection types:

    bi@(A b -> C /\ C b -> D) => A b -> D
    tri@(A b -> C /\ C b -> D /\ D b -> E) => A b -> E

If you want, I can get into the reason this is necessary. For now,  
back on track.

An important thing to note about 'cleave' and '2cleave' is that they  
don't care at all what the arities of the functions are; they push on  
one (or two, in the case of '2cleave') elements for each quotation  
regardless. For example, these are all equivalent:

   5 cleave(neg, *) = 5 bi(neg, *) = 5 keep(neg) * = 5 neg 5 * = -25

Alright, now onto the "type-directed cleave". Agh, so much to say.

I came up with the idea for a type-directed cleave when trying to  
figure out the concatenative equivalent of the functional form of  
"construction" from Backus's FP. What construction does is take  
several functions and some 'x' and return an array of all of the...  
well, I'll just show it:

    [neg, sqr, sqrt]:9
    [neg:9, sqr:9, sqrt:9]
    [-9, 81, 3]

It turns out that construction is the same as the normal 'cleave':

    9 cleave(neg, sqr, sqrt)
    9 neg 9 sqr 9 sqrt
    -9 81 3

In FP, there's no way to have something like '2cleave'. What you need  
to do is bundle the two arguments into an array and use a combination  
of construction and "insert" (which is the same as 'foldl1' in  
Haskell, i.e. a fold with the head of the list as the initializer). To  
be clear, the slash denotes the "insert":

    [/+, /-, /*]:<5, 3>
    [/+:<5, 3>, /-:<5, 3>, /*:<5, 3>]
    [5 + 3, 5 - 3, 5 * 3]
    [8, 2, 15]

This works, but the downside is the need to explicitly use insert for  
each function. The equivalent in 5th using cleave is rather ugly (and  
grossly inefficient):

    (5, 3) cleave(reduce(+), reduce(-), reduce(*))
    (5, 3) reduce(+) (5, 3) reduce(-) (5, 3) reduce(*)
    5 3 + 5 3 - 5 3 *
    8 2 15

Hence, we have '2cleave', which doesn't require the bundling and  
inserting/reducing of the FP and 'cleave' versions:

    5 3 2cleave(+, -, *)
    5 3 + 5 3 - 5 3 *
    8 2 15

Anyway, the point of the "type-directed cleave" was to be able to  
write ' 5 3 [+, -, *]' and have it magically work. Of course, in order  
to know how many items to copy and push on for each function, you need  
to know their arities. There are *three* ways you can go about doing  
this.

The first way is to require all functions have the same arity (at  
least in terms of what they consume -- the productions don't matter).  
This works great for the '[+, -, *]' example, but it's a bit  
restrictive.

The second way is to allow each function to have a different arity. In  
order to figure out how many items to save, you save as many as  
necessary for the function with the greatest arity. For example:

    2 3 4 [+, dup, rot]
    2 3 4 + 2 3 4 dup 2 3 4 rot
    2 7 2 3 4 4 3 4 2

The third way is the most bizarre, but perhaps the most useful. What  
you do is save as many items as necessary for the function with the  
greatest arity. However, for each function, you only push on as many  
items as it directly uses. For example:

    2 3 4 [+, dup, rot]
    3 4 + 4 dup 2 3 4 rot
    7 4 4 4 2 3

Of the three, the third possibility, I think, is the most useful; the  
second is awkward to use in many cases and the first is just a special  
case of the second and third.

Now, there are a couple things to point out here.

With the normal 'cleave', as shown earlier, we can do things like '5  
cleave(neg, *)' to get '-25. This is impossible with any of the type- 
directed variants above. This isn't really a big problem however as '5  
cleave(neg, *)' is really misusing 'cleave'. It should be written as  
'5 keep(neg) *' instead.

The other, and much more important thing thing to note, is that it is  
impossible to figure out how many items to save and push on for the  
first two type-directed variants unless at least one function is  
provided. In other words, you can't give a rewrite rule or any sort of  
dynamic semantics for this definition of 'foo':

    foo(a, b) = [a, b]

However, with the first two type-directed variants, you could for this  
definition of 'bar':

    bar(a, b) = [a, b, +]

The rewrite rule here would be 'x y (a, b) = x y a x y b'. Again,  
without the additional context given via '+', there's no way to do  
this for the first two type-directed variants.

For the third type-directed variant, there's no way to give a dynamic  
semantics unless *all* functions are provided. As interesting as this  
third variant is, it is just too powerful to include in the language.  
Of the three, only the first is really sensible. In that case however,  
all it does is save you a character so that you can avoid specifying  
the arity of the quotations.

I don't think saving that character is worth the clutter in the  
language definition. It would mean you can't look at the type system  
as merely a means of restricting the dynamic semantics of the  
language. This is the same nasty situation Haskell is in with respect  
to type classes. Type classes, however, at least provide some real  
expressive powerful.

To very briefly clarify on what I mean about the type system no longer  
simply restricting the language, I'll give a brief example. Let's say  
we write something like this:

    id1 = dup drop
    id2 = swap swap

All 'id1' does is ensure one element is on the stack. All 'id2' does  
is ensure two elements are on the stack. On a stack with many items,  
they're identical. However, when using them in a type-directed cleave,  
they have different properties. For example, '1 2 3 type-cleave(id1,  
id1)' is not the same as '1 2 3 type-cleave(id2, id2)', even though  
they would be with a normal cleave.

It should hopefully be obvious now that, since a type-directed cleave  
is essentially meaningless unless sufficient context is given, that we  
also can't type it without that same context. I hope it is also clear  
why I said in the previous email that they're almost certainly a bad  
idea. I apologize for taking the *long* way to get to this conclusion,  
but I'm far too tired to write something concise.

- John
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.