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: pointfree may not be the best name, but it won't be taken for other reasons...and reinforces the term for Rebol programmers unused to FP concepts. There may be a nicer name.)

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 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 the duality between FRAME! and ACTION as "antiform FRAME!". This provides a convenient round-trip:

>> data: [a b c]

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

>> apd: runs 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

Below is a draft usermode implementation. It is incomplete and raises some issues. The biggest issue is that it only lets you specialize by example at one level of depth. So you can't write:

>> apde-spread: pointfree [append _ spread]

>> apde-spread [a b c] [d e]
== [a b c d e]

That would require some kind of recursive process--the call to SPREAD in the parameter slot for APPEND would have to be done using POINTFREE as well.

Moreover, there's a semantic question of when to capture variables. If you write:

>> x: 10

>> add-x: pointfree [add x]

>> x: 20

>> add-x 1000
== ???

...do you want to add the value of X as it was at the moment of specialization, and get 1010? Or to add its current value and get 1020? If you wrote out a function you'd get 1020, and I'd think the more common desire (and you should have to say pointfree compose [add (x)] to get the former intent). But as written now you get 1010, so a different mechanism is needed.

All that said, this implementation shows relatively little code, that's 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* Is Lower-Level, Takes Applicand Separate From Args

This layer takes FRAME! and BLOCK! as two separate arguments. If the user does:

>> apde: pointfree* :append [_ [d e]]

Then the blank signals an unspecialized slot. So that would be like writing:

>> apde: specialize :append [value: [d e]]  ; leave series unspecialized

>> apde [a b c]
== [a b c [d e]]

Note that Variadics are not supported at this time. And there's an ongoing effort to improve parameter reflection, but this is what we have at the moment.

pointfree*: func* [
    "Specialize by example: https://en.wikipedia.org/wiki/Tacit_programming"

    return: [action?]
    frame [<unrun> frame!]
    block "Invocation by example (BLANK!s are unspecialized)"
        [block!]
    <local> params var
][
    frame: copy frame  ; if given e.g. APPEND's frame, don't mutate it!

    for-each param (parameters of frame) [
        if tail? block [break]  ; no more args, leave rest unspecialized

        match [word! lit-word? get-word!] param else [
            continue  ; skip unused refinements
        ]

        case [
            blank? block.1 [  ; means leave unspecialized
                block: next block
            ]

            match word! param [  ; ordinary parameter
                [block frame.(param)]: evaluate/next block
            ]

            all [
                match [get-word!] param  ; soft literal parameter
                match [group! get-word! get-tuple!] block.1
            ][
                frame.(param): reeval block.1
                block: next block
            ]

            <default> [  ; hard literal or non-escaped soft literal
                frame.(param): block.1
                block: next block
            ]
        ]
    ]

    if not tail? block [
        fail/blame [
            "Unused argument data at end of POINTFREE block"
        ] $block
    ]

    return runs frame
]

Higher-Level POINTFREE Includes the Applicand In The BLOCK!

This higher-level version wants the name of an action at the head of the block, e.g.

pointfree [append _ [d e]]
=>
pointfree* :append [_ [d e]]

It does this by specializing out the frame on the interface of POINTFREE* and just taking a block. Then when it's called, it overwrites the dummy specialization with a frame from the first block element, and updates the block to be one step forward.

pointfree: specialize (adapt get $pointfree* [
    frame: (match frame! any [  ; no SET-WORD! namecache
        if match [word! path!] block.1 [
            unrun get/any inside block block.1
        ]
    ]) else [
        fail "POINTFREE requires FRAME! argument at head of block"
    ]

    block: next block
])[
    frame: unrun get $panic/value  ; overwritten, best to be something mean
]

A post was split to a new topic: Shorthand Interface and Symbol/Name for POINTFREE

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

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