Implicit Execution of RETURN in functions = BAD

Let's say you write something like this:

foo: func [
    return: [integer!]
    arg [integer! text! tag!]
][
    if integer? arg [
        return arg + 1000
    ]
    if text? arg [
        return reverse arg
    ]
    arg
]

I imagine you'd expect behavior along these lines:

>> foo 20
== 1020

>> foo "whoops"
** Error: FOO doesn't have RETURN enabled for values of type TEXT!

>> foo <bomb>
** Error: FOO doesn't have RETURN enabled for values of type TAG!

Even though the last value just "falls out" of the function, you presumably don't want that to mean it escapes type checking just because it did.

Mechanically, this is Non-Obvious...

RETURN is not actually supposed to be a "language feature" per se. It's actually a feature of the higher-level generator FUNC...and there are lower-level ways of building functions that lack RETURNs. (If there weren't, how could you write the RETURN function itself?)

Plus it's fully overrideable. You can set RETURN to some random integer if you feel like it...it's just a variable in the frame. But more frequently you'd like to specialize or adapt it:

bar: function [
    return: [integer!]
    arg [logic!]
][
    return: adapt :return [value: value + 300]
    if arg [
       return 4
    ]
    720
]

>> bar true
== 304

...but here we are at an interesting question. What do you expect to happen with bar false?

>> bar false
== 720  ; (A) values falling out of bottom *do not* run RETURN implicitly

>> bar false
== 1020  ; (B) values falling out of bottom *do* run RETURN implicitly

A usermode implementation of FUNC has a pretty easy trick to implement either behavior. The question is simply if it takes the body you give it and turns it into a GROUP! and passes it to RETURN or not...

"real body" option (A) for BAR above

[
    return: (make action! ...)  ; low level make return function
    (  ; no return here, just `as group! body`
        return: adapt :return [value: value + 300]
        if arg [
            return 4
        ]
        720
    )
]

"real body" option (B) for BAR above

[
    return: (make action! ...)  ; low level make return function
    return (  ; automatic return injected
        return: adapt :return [value: value + 300]
        if arg [
            return 4
        ]
        720
    )
]

Or Just Require RETURN with FUNC that has RETURN: ?

strict option (C) for BAR above

[
   return: (make action! ...)  ; low level make return function
   (  ; automatic return injected
       return: adapt :return [value: value + 300]
       if arg [
           return 4
       ]
       720
    )
    fail "Functions which specify RETURN: must use RETURN"
]

I've brought this up before, and @IngoHohmann and @iArnold seemed to think "dropping out the last value" was somehow fundamental.

But given what I say above about how the semantics get pretty sketchy on type checking and such, what if we say that functions that don't specify RETURN have no return available, and just drop out their last result?

bar1: function [
    arg [logic!]
][
    if arg [4] else [720]
]

>> bar true
== 4

>> bar false
== 720

You don't get type checking so you'd have to do it yourself, which will be available:

bar1-checked: function [
    arg [logic!]
][
    let val: if arg [4] else [720]
    ensure integer! val
]

However: if you specify RETURN: in the spec then you are required to use it. This gives you type checking and guarantee of running any return hooking on all code paths:

bar2: function [
    return: [integer!]
    arg [logic!]
][
    return: adapt :return [value: value + 300]
    if arg [
       return 4
    ]
    return 720
]

>> bar true
== 304

>> bar false
== 1020

I Think The Case for Requiring RETURN if RETURN: Is Strong

It seems rather clear when laid out like I have above that it's the right answer. People who hook RETURN are typically doing so because they want it on all return paths. But I think hiding a RETURN behind the scenes is a cognitive time bomb. Being explicit sorts that out, and it also provides a rational answer for why you get type checking...the RETURN does the check.

(I can tell you that without that rational answer, the internals have ugliness. This policy will cleanse the ugliness.)

Lower-level functions that don't have RETURN have to have a way to return values. Dropping them out the bottom seems a good way to start building up the mechanic. It's also useful for quick and dirty "macro-like" functions, so I see no problem with that.

Should return: <void> and return: <none> be Exceptions?

The point of introducing these cases was to help remove concern over letting unwanted variables "fall out", and being able to forego concerns about type checking. It's a different case, because there is no type checking involved of the parameter passed to RETURN as it takes no parameters...and there's no need to type check what falls out the bottom because it is discarded:

something: func [
    return: <none>
    value [text!]
][
    if value = "" [return]  ; this RETURN acts as `return ~none~`
    append data value

    ; expectation has been that this would also yield ~none~
]

The idea was to make it painless to shield callers from seeing the returned result, and have them know there was no result they were supposed to pay attention to.

What gets me concerned here is that question of whether or not a hooked or modified RETURN is implicitly run at the end of such a function.

It seems to suck to have to put the RETURN there. :frowning:

something: func [
    return: <none>
    value [text!]
][
    return: adapt :return [print "I AM RETURNING!"]
    if value = "" [return]
    append data value
    return  ; with this here, it's clear you will get the PRINT to happen
]

But it does benefit from the explicitness. There's no ambiguity.

Again, you have to use your imagination to think about a longer function in which there are many control paths through the function...and someone decides to hook RETURN. If you are working in a large codebase with long functions, wouldn't you like to know that all control paths will run your hook...and that the language has gotten everyone on the same page that is expected and possible?

My feeling in the moment is that the only answer I'd consider besides erroring if there's no RETURN would be to implicitly put a RETURN at the end, so a hooked RETURN would be executed if a value drops out the bottom. But I've explained that for the other cases I think that's sneaky. It feels much more forthright to have the call at source level.

Path of least resistance on this is to add the RETURN implicitly, so I'm going with that.

1 Like