Should RETURN be Assignable on Function Interfaces?

When implementing multi-returns I had an idea, to think of [x]: negate 10 in terms of slipping X into the RETURN: slot of NEGATE as an input.

On the surface that seems like it might even be useful more generally:

 >> f: make frame! :negate

 >> negate.return: 'x  ; X gets hidden, and RETURN is redefined during the call

 >> negate.value: 10

 >> do f  ; the hidden X is written back automatically
 == -10

 >> x
 == -10

But Not Every Action is a FUNC/FUNCTION

Not all functions are guaranteed to have something in their frame called RETURN (e.g. a LAMBDA does not). And a non-FUNC ACTION! could have something in its frame called RETURN that wasn't used for anything pertaining to the return process.

This ruled it out from being a mechanic relied on by the multi-return machinery. Because we want the following to work:

>> test: lambda [x] [x + 20]

>> [y]: test 1000
== 1020

>> y
== 1020

Nevertheless, People Can Implement It If They Want

It's certainly something you could choose to do if you were writing your own function generator.

In fact, it's trivial to write a wrapper for it! Just add a /RETURN to the public interface, and write back to it if it's supplied:

returnproxy: lambda [action [action!]] [
    enclose (augment :action [/return [word!]]) f -> [
        (maybe f.return): do f

That means you can pass it as a refinement:

>> test: lambda [x] [x + 1000]

>> wrapper: returnproxy :test

>> wrapper/return 20 'y
== 1020

>> y
== 1020

Or you can use it with a frame:

>> f: make frame! :wrapper
>> f.x: 20
>> f.return: 'out

>> do f
== 1020

>> out
== 1020


(Seeing superpowers like this work so clearly and obviously is what keeps me invested in this.)

So Anyone CAN Do It, But Should FUNC/FUNCTION Do It?

I lean toward not doing it with things implemented the way they are right now, because it would add overhead to every function with a RETURN:, due to needing to have a place to store the variable if you gave it one.

But it might be nice to give people an optimized version of the proxying wrapper above. You could then convert any function to support it.

But today RETURN is on the public interface of FUNC :frowning:

>> f: make frame! func [x] [return x + 1000]
== make frame! [
    return: ~
    x: ~

"Errr. Why's it there?", you might ask.

The reason is that the return typeset information currently lives on that field. So if you're going to ask about it, you get it from there.

This is one of many good arguments for why this information should not live there.

There's a long running body of evidence suggesting that the way return types are managed today is probably wrong.

  • LAMBDA Can't Currently Document Its Result Types. There are a lot of actions out there that don't have a RETURN function, but nevertheless have something to say about what types they can produce.

  • ENCLOSE Can't Change The Type Signature. If you wrap a function, you're subject to its type checking rules.

  • NATIVE Doesn't Want To Pay For A RETURN Slot. The typechecking is only done in the debug build, so why should every native frame require a RETURN function?

I think I've got some ideas coming together--mostly centering on factoring out typechecking to be another one of the little pieces you can build functions out of (like AUGMENT). So when doing a composition you would just bolt on a typechecker if you wanted one. Internal efficiencies could fold that in so it actually didn't generate a separate phase and action identity. Pursing some inspiration on that as we speak...

1 Like