Looking into a *usermode* POINTFREE

The concept of "Point Free" or "Tacit Programming" is that it's a way of relating functions to each other without mentioning their arguments.

So instead of writing something like:

 append-to-data: function [value] [append data value]

You'd have some way of avoiding the redundancy of having to say "value" twice. So imagine declaring an identical function with a syntax like:

 append-to-data: pointfree [append data]

That would notice you didn't provide all the parameters to append, so an implicit parameter to the new function would be added. But what if you wanted instead something like:

append-10: function [series] [append series 10]

So maybe you would have some syntax in POINTFREE for that where you put a placeholder in spots that you wanted implicitly picked up. Maybe BLANK! as a default:

append-10: pointfree [append _ 10]

(Note: The name pointfree is deliberately chosen as bad, since it won't be taken for other reasons...and also reinforces the term for Rebol programmers unused to FP concepts. What I'm actually thinking is that this will be folded into lambda as the behavior when you don't use a block. So (=> append _ 10) for example.)

Writing This In Usermode In Historical Rebol Is Non-Trivial

If you think writing a reliable POINTFREE yourself would be easy in Rebol2/Red, I would invite you to try. There's a lot to get right with parameter analysis and ordering refinements; beyond the average user. It doesn't get much easier if you are coding inside the system, either.

It would also be noticeably slower, using any method in near-reach. The only mechanisms would involve creating a whole new function spec, where the evaluator would have to pass parameters to that function...then start evaluating again to make a nested call...type checking all over again.

Ren-C has several tricks up its sleeve, including a new tool introduced today for writing your own specializations. That's the ability to MAKE ACTION! out of a FRAME! with some of its parameters filled. And since you can MAKE FRAME! out of an ACTION!, this provides a convenient round-trip:

>> data: [a b c]

>> f: make frame! :append
>> f/series: data

>> apd: make action! f
>> apd [d e f]

>> data
== [a b c d e f]

There's also a great convenience afforded by Ren-C's smart specialization handling. Consider:

>> parameters of :append
== [series value /part /only /dup /line]

>> apdup: :append/dup

>> parameters of :apdup
== [series value dup /part /only /line]

So if you have someone writing pointfree [append/dup ...] you don't worry...just GET the PATH!, and the function you are handed back doesn't even report DUP as a refinement. It's a normal parameter now.

There's also more unlocked by the idea of refinements naming their 0-or-1 arguments. We've seen it open doors with AUGMENT, but it helps here and elsewhere.

A Start on the Vision of POINTFREE

This usermode implementation is incomplete and raises some issues (including that I'm not totally thrilled with the @var skippable syntax in EVALUATE).

But it shows relatively little code taking care of some rather complex acrobatics. I'd hope it wouldn't be too far beyond the reach of a novice to write:

pointfree: func [
    {Specialize by example: https://en.wikipedia.org/wiki/Tacit_programming}

    return: [action!]
    block [block!]
][
    let action: (match action! any [
        if match [word! path!] :block/1 [get block/1]
        :block/1
    ]) else [
        fail "POINTFREE requires ACTION! argument at head of block"
    ]
    block: next block  ; rest of block is invocation by example

    ; If we did a GET of a PATH! it will come back as a partially specialized
    ; function, where the refinements are reported as normal args at the
    ; right spot in the evaluation order.  (e.g. GET 'APPEND/DUP returns a
    ; function where DUP is a plain WORD! parameter in the third spot).
    ;
    ; We prune out any unused refinements for convenience.
    ;
    let params: map-each w parameters of :action [
        match [word! lit-word! get-word!] w  ; !!! what about skippable params?
    ]

    let frame: make frame! :action  ; all frame fields default to NULL

    ; Step through the block we are given--first looking to see if there is
    ; a BLANK! in the slot where a parameter was accepted.  If it is blank,
    ; then leave the parameter null in the frame.  Otherwise take one step
    ; of evaluation or literal (as appropriate) and put the parameter in the
    ; frame slot.
    ;
    let var
    iterate params [
        case [
            blank? :block/1 [block: next block]

            word? params/1 [
                if not block: evaluate @var block [
                    break  ; ran out of args, assume remaining unspecialized
                ]
                frame/(params/1): :var
            ]
            
            all [
                lit-word? params/1
                match [group! get-word! get-path!] :block/1
            ][
                frame/(params/1): reeval :block/1
                block: next block
            ]

            default [  ; hard literal argument or non-escaped soft literal
                frame/(params/1): :block/1
                block: next block
            ]
        ]
    ]

    ; We now create an action out of the frame.  NULL parameters are taken as
    ; being unspecialized and gathered at the callsite.
    ;
    return make action! :frame
]

I've committed this experiment, and further the behavior of lambda to do it when the right hand side is not a block:

bump-up: (=> add 1)
bump-down: (=> subtract _ 1)

There is some error handling:

>> ap12invalid: (=> append _ 1 2)
** Error: Unused argument data at end of POINTFREE block

>> ap1twice: (=> append/dup _ 1 2)

>> ap1twice [a b c]
== [a b c 1 1]

So if you want to mess with it and offer comments (e.g. @IngoHohmann, @giuliolunati, @orr721 then do). Also Giulio...if you didn't notice, the pthreads build should be running again so give that a shot.

It's Time To Start Looking for the Complexity Limit

I actually started writing this feature in C, before stopping and saying "wait, the whole point is that a user should be able to write this".

So that's when I realized it was time to look at the MAKE ACTION! from a FRAME! feature. And there's a laundry list of ways in which it doesn't quite combine right with variadics or enfix or skippable parameters etc...or functions you want to actually pass null to.

But I don't want to address those problems by writing POINTFREE as a native. I want to stick to the idea that it can be done accurately in usermode.

This is challenging stuff, but still very cool.

2 Likes

I was noodling on this, trying to get the concept boiled down to a more concrete visual. The most familiar of the Wikipedia examples was the UNIX pipe:

I was momentarily disappointed that you did not implement it in C, because then I would have had a better chance of understanding what was going on. A selfish thought, I know. Though I do tend to "think" in C as it is the highest level language in which I can reasonably assume what the underlying hardware is doing in order to honor what was specified in the imperative-procedural program. C to asm is relatively straight forward. So it is a sort of a short cut for the conceptual model.

As I envision how to implement POINTFREE in the procedural paradigm, I suspect control-flow would get inverted as is often done in C driver interfaces with function pointers. IIRC that is how the UNIX pipes are done using the read()/write() syscalls for passing stdout to stdin (without making a copy of the data).

So the problem reduces to a buffer that gets passed around and modified. The challenge is maintaining type information. These solutions tend to be domain specific and never flexible enough to handle all or most of what the clever electrical and computer engineers and scientists of old have abused the ordering of bits to represent values of things. I believe this is what led to the notion that Alan Kay was going after when he coined "object-oriented", the idea that the data comes with the code necessary to use the data. It's deeper and hairier than that, but at a first approximation, about it.

Onward. =)

Cheers,
Joe Gorse

1 Like

Having to use PowerShell at work, I really do like the way you can compose with pipes, where you can stick a new step at the end.
With rebol you sometimes have to bracket the next step around your current evalutions, because of parameter order.

1 Like