Introducing REFRAMER: Close Cousin to ENCLOSE

This is a very cool tool that might help concretize some of the abstract-sounding arguments I was making about frames...

Reframers build a frame from whatever follows them at a callsite, and can operate on it before running it (if it runs it).

Defining a reframer involves giving it a function that will act as the "shim". Here is a reframer using a very simple shim, that just takes in the frame and returns it.

get-frame-of: reframer (func [f [frame!]] [f])

Here is what happens if the thing that followed that reframer's execution was a call to APPEND:

>> get-frame-of append [a b c] <d>
== make frame! [
    series: [a b c]
    value: <d>
    part: '
    only: '
    dup: '
    line: '
]

As we can see, it gathered the arguments for the APPEND and put together a FRAME! to represent the call.

The reframing process did not automatically execute the frame. But the "shim" function can!

Let's try something that runs the function twice.

>> two-times: reframer func [f [frame!]] [do copy f, do f]

>> two-times append [a b c] <d>
== [a b c <d> <d>]

Whoa.

But wait, there's more. Besides not automatically executing the FRAME!, it also doesn't typecheck it (yet).

>> get-frame-of append 1 <d>
== make frame! [
    series: 1        ; !!! this wouldn't be legal to run as-is 
    value: <d>
    part: '
    only: '
    dup: '
    line: '
]

With our "shim" function in the driver's seat, it can manipulate the inputs and the results.

Consider how right now, functions like APPEND won't take QUOTED!

>> item: first ['''[a b c]]
== '''[a b c]

>> append item <d>
** Script Error: append does not allow QUOTED! for its series argument

But what if we made a REQUOTE that:

  • would build a frame for whatever follows it
  • counted how many quoting levels were on the first argument in that frame
  • took the quoting levels off that first argument
  • ran the function
  • added the quoting levels back to the result

It should be an easier function to write than it is, but even so it's not that hard:

requote: reframer func [
     {Remove Quoting Levels From First Argument and Re-Apply to Result}
     f [frame!]
     <local> p num-quotes result
][
    p: first words of f
    num-quotes: quotes of f/(p)
    f/(p): dequote f/(p)

    if null? result: do f [return null]  ; exempt NULL from requoting

    return quote/depth get/any 'result num-quotes
]

And behold:

>> item: first ['''[a b c]]
== '''[a b c]

>> requote append item <d>
== '''[a b c <d>]

The Shim Can Take Arguments

The last argument of the shim needs to be a frame, but it could also have its own arguments.

Just to demonstrate this point without any frame-fiddling to obscure the point, how about a message that prints before and after just executing the frame:

>> bracketer: reframer func [msg [text!] f [frame!]] [
       print msg
       do f
       print msg
   ]

>> bracketer "Aloha!" print "I'm being framed!"
Aloha!
I'm being framed!
Aloha!
== ~void~

Things To Think About

This is very cool, and it pins down a number of questions about evaluation.

Hopefully now you can see why type errors shouldn't happen during argument fulfillment, but only once the function actually gets to the point of running. e.g. a reframer that just does the function after it shouldn't act any different than that function would running normally.

One tough problem is what to do when you get multiple reframer functions in a row. I give the example of:

>> item: first ['''[a b c]]
== '''[a b c]

>> item: my requote append <d>
 ; ... how can this work?

MY is also a reframer. But if it gets a FRAME! for REQUOTE, that will not be what it expects. Because REQUOTE has a single argument in its frame...which is a frame, not a callsite argument. :-/

What MY really wants is a FRAME! for the aggregate function of "REQUOTE APPEND". Such aggregate frames aren't impossible to conceive of, but are beyond what we have today.

That's probably the biggest issue I can see right now with this. But it's a step ahead of having to reinvent the technique on every function that wants to do something like it. And it means that when an answer for one such functions is made, all of them will get it.

2 Likes

The ideas behind REFRAMER had percolated around the codebase as experiments. For instance in the implementation of DOES it would specialize by example--without needing a block:

>> foo: does [catch [throw <with block>]]
>> foo
== <with block>

>> foo: does catch [throw <without block>]
>> foo
== <without block>

While it's neat, it opens a can of worms. does reverse block and does (reverse block) then mean entirely different things. The first makes a function that reverses a block on each call and returns that block (as if you'd written DOES [REVERSE BLOCK]). The second reverses a block once at creation time to get some backwards code, and the function runs that backwards code on each call.

The worm-can doesn't end there! does reverse block and does [reverse block] are still subtly different. The former won't react to changes in the BLOCK variable while the second one will. That's because the first is actually a fast form of specialization of REVERSE, where the fetched BLOCK! value has been slotted into the frame.

The weirdness of frame-making-based-on-callsites spread around the codebase in other places that wanted to try similar tricks...such as MATCH.

REFRAMER Isolates The Weirdness

With a reframer, you can implement the weird DOES feature easily if you want it, by saying:

>> does+: reframer func [f [frame!]] [print "Reframing", does [do copy f]]

>> foo: does+ catch [print "Calling", throw <like this>]
Reframing

>> foo
Calling
== <like this>

>> foo
Calling
== <like this>

I think this is a smarter direction; to build these capabilities in userspace, and to consolidate the functionality for implementing them in REFRAMER. So I'm culling it from the default DOES and MATCH.

(To state the maybe-not-obvious (?): the code for REFRAMER came from unifying the needs of these experiments, and noticing what they have in common. So while it may seem these things "change a lot and then change back", the process produces net benefits...)

I've realized that with the ability to reorder parameters, we can have a solution for these aggregate frames (!).

The reframer simply adds arguments to the end of the frame (like an AUGMENT) so the frame can be subsetted and still compatible with the reframed function. But then it adds the reordering information so its arguments go first.

Not only is it not far out of reach, this makes it seem like there is no such thing as REFRAMER...it's just ENCLOSE which doesn't do type checking until you actually DO the embedded frame. Basically it just becomes ENCLOSE/NOTYPECHECK!

This is pleasing. You aren't getting these "second class citizen reframer functions" which are limited in how you can work with them. It's pleasing in the same way that being able to add parameters to the interface of functions which have locals of the same name is pleasing. Once you put something in the domain of ACTION!, it has the properties of any other action with the same interface. Shades of monads. :stuck_out_tongue:

I'm tempted to attack this right now, but I'm in the middle of something else that is also really cool.

2 Likes