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.)

UPDATE: To eliminate potential accidents, it was unbundled from lambda and uses the similar-but-notably-distinct notation <- now.

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 given it the notation <-

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

POINTFREE is pretty awesome and I find it comes in handy a lot.

I had even taken the step of folding its behavior into lambda (which was recently renamed from the JavaScript-like => to the Haskell-like ->... for a couple of plausibly-good reasons):

>> ap10: (-> append _ 10)

>> ap10 [a b c]
== a b c 10

But it does make me somewhat uneasy with all the variadic voodoo to make that happen. I'm especially concerned that people might mix them up, and do a POINTFREE specialization when they meant a lambda, simply because they forgot to put in a BLOCK!.

I've had a hard time imagining what <- should do... especially because if -> is a function generator, it would seem kind of weird if <- was something vastly different. So I think this points to a balanced choice to turn this around so that <- is the very "weird" variadic POINTFREE generator, while -> is lambda, and always requires a BLOCK! of code on its right.

>> whatever: x -> [print ["lambda always goes to a block" x]]

>> ap10: (<- append _ 10)  ; pointfree never has anything on left

>> ap10 [a b c]
== a b c 10

Seems learnable enough?

2 Likes

POINTFREE would not have to deal with the placeholder syntax, if you provide a FLIP that exchanges the order of the arguments of a function. Then

append-10: pointfree [flip append 10]

P.S.Richard Bird, Oege de Moor. Algebra of Programming. It's quite old, but if you haven't seen it yet: I think it's very much worth reading.

One of the big ideas here is giving people options vs. there being a "one way to do it", and I think having placeholders would fit a lot of people's intuition better than other transformations...

However... parameter reordering is an important thing, and it's been slowly moving up on the agenda...so I went ahead and added a REORDER primitive.

It doesn't have quite the syntax form you used with FLIP in the box. But it's a building block for making FLIP-like things.

I actually put in a little bit of a hacky twist to REORDER so that it would ignore refinements. That way you could do things like REVERSE a parameter list. e.g. when you ask for PARAMETERS OF the APPEND operation, you get the optional parameters too:

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

So if you tried to build a FLIP command that reversed the parameter list, you'd have to filter them out. But having reorder do that automatically makes it a bit easiser.

>> reeval reorder :append (reverse parameters of :append) <item> [a b c]
== [a b c <item>]

Notice that making a function doesn't re-dispatch it, so you have to reeval-uate it. It works for other types, too.

>> reeval second [a: b: c:] 10 + 20
== 30

>> b
== 30

POINTFREE is still under development thought here (with interim experiments in various levels of disrepair). But in the meantime we have MACRO:

flip: macro ['name [word!] <local> action] [
    action: ensure action! get name
    reduce [reorder :action (reverse parameters of :action)]
]

I have it do the quoting (so it takes ['name [word!]] instead of [action [action!]]... so that the call matches your example's syntax:

>> flip append <item> [a b c]
== [a b c <item>]

>> flip subtract 10 1
== -9

Oege worked with my group briefly at Microsoft Research many, many years ago...seems he's gone on to building a fairly popular thing (LGTM). I tried to use it, but it doesn't work on forked repos. Maybe I'll try it again when this project gets renamed/re-launched...whenever that is... :hourglass:

2 Likes

POINTFREE is very cool, but I'm removing its usage from bootstrap...because changes I want to make break it. And it's hard to fix and redesign such things when the system won't boot.

While I could remove its bootstrap usage... and then make my next task after the changes be to work through it and put it back in the bootstrap, I'm actually not going to. I'm just going to leave in a file on the side, as part of the test scripts--for now. I'm going to leave it broken until such time as it's a priority to fix it.

I can't make everything a focus, and am mentioning this just to talk out loud about the need to become more mercenary.

The big "what's possible in usermode" showcase is UPARSE. And it stands on the shoulders of all the efforts like POINTFREE that came before it. But the demand to keep all of these experiments working -and- try using them in the boot process is too much.

2 Likes