Making FUNC Variant That Auto-Returns Last Result

I had offered that people could use this simple wrapper to get the old behavior of FUNC, to drop out its last result (instead of returning trash):

func: adapt :lib.func [body: compose [return (as group! body)]]

The concept is that if you write:

 foo: func [x] [
     if x < 0 [return x + 10]
     x - 10
 ]

What you'd actually get would be:

 foo: lib.func [x] [
     return (
         if x < 0 [return x + 10]
         x - 10
     )
 ]

Broken Under New Binding Model

Under the new binding model, an array which is bound will by default hold onto its binding.

  • The BODY was a BLOCK! that evaluated at the callsite and captured its environment (e.g. to the user context, module context, outer function context, etc.)

  • When you convert it to a GROUP! it will still have that binding.

  • The surgery which is done to inject the frame containing X into the environment will thus apply only at the outermost level. So the outer block where the return lives would see it, but never make it to the inner group.

    func: adapt :lib.func [
        body: compose [
            print ["Here would see the X:" x]
            return (as group! body)  ; <-- inside group will not
        ]
    ]
    

Mimicking FUNC's Surgery

One idea would be to use a utility that can do something like what LIB.FUNC did to put the frame onto the proxy body, on the original body.

But awkwardly, you can't do this surgery inside the COMPOSE, because it has to be done once the function is running. If we call that surgery OVERBIND, one answer would look something like this:

func: adapt :lib.func [
    body: compose [
        return do overbind binding of in [] 'return (body)
    ]
]

(I'll mention that this OVERBIND is not as efficient as what FUNC does, because the frame itself has a pointer to the inherited environment when it's made. But it only has one slot for that pointer. So if you try to build a specifier chain with the frame that points to another environment, it has to fabricate a "frame holder" which has its own pointer to put in the chain.)

Notice that we only want the frame, here... which we get from the binding of the RETURN. We don't want to inject awareness of everything in this FUNC adaptation's scope. So not overbind [] (body). If such a thing were even legal to do... body probably has its own copy of lib inherited from another module, and then you've got another module and its lib... we're trying to avoid that kind of conundrum with conservative binding preservation.

Another Approach: Steal The Binding And Unbind

Another approach would be to steal the binding off of the body and put it on the composition, then remove the binding from the body:

func: adapt :lib.func [
    body: in body bindable compose [
        return (bindable as group! body)
    ]
]

You can put the BINDABLE before or after the AS GROUP!.

I actually think this reads less insanely if we just go with UNBIND as being a "tip-unbinding" operation by default, and then have UNBIND/DEEP. (Maybe UNBIND/SHALLOW for one level of depth?):

func: adapt :lib.func [
    body: in body unbind compose [
        return (as group! unbind body)
    ]
]

It's much better to do it this way. You're doing cheaper operations and doing them at FUNC creation time.

Note That I Still Hate Implicit RETURN

There's a reason the default FUNC doesn't do it. If you start doing cool things with RETURN you'll break things in a way that lurks.

Sample cool thing that works today:

 foo: func [x y] [
     return: adapt augment :return [arg2] [
         value: meta ((unmeta value) + arg2)  ; return ^VALUE is meta
     ]
     if x > 10 [
         return x y  ; arity 2 return, adds args and returns sum!
    ]
]

>> foo 20 30
== 50

>> foo 5 5
== ~  ; anti

If anything, I feel like the trash result isn't going far enough--and it should actually error if you skip out on a return. Maybe there was some important finalization work to do.

But my example shows that putting it in implicitly means it doesn't necessarily roll with whatever updated definition you give to return. You'll just get a confusing error in a bit of hidden code. Not good for a fundamental part.

1 Like

Iā€™m not sure I understand what this proposed UNBIND/DEEP would do. Would it remove the bindings from all nested BLOCK!s?

I like this solution! This feels like a good use of UNBIND to me ā€” removing the environment from one point to re-add it elsewhere.

A more elaborate version was created at one point, that turns RETURN arity-0 when you say return: [~]

adapted-func-body: [
    body: if find spec spread [return: [~]] [
        compose <*> [  ; only compose groups marked with <*>
            return: adapt augment (
                specialize :return [value: meta ~]
            ) [^end-test [<end> any-value!]] [
                if not null? end-test [
                    fail 'end-test [
                        "arity-0 RETURN enforced when return: [~]"
                    ]
                ]
            ]
            (<*> as group! body)  ; returns in body will be arity-0
            return  ; all functions must return (in case hooked)
        ]
    ] else [
        compose [return (as group! body)]
    ]
]

func: adapt :lib.func adapted-func-body

This shows the weakness of the approach of proxying the binding when you're using any words besides the words you know will be in the frame.

The usage wasn't critical, so I took it out of the file it was in and put it here for further thought.