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

I've been back to working on stackless, and am dead-set on getting it merged in.

This puts me face to face with some tough issues surrounding function implementations and RETURN.

No Feedback Here So I Have To Go After It

@gchiu has been living under a rock (well, in New Zealand :sheep: same difference). So he never even noticed Ren-C supports RETURN: specs. Hence any RETURN he's ever used has been in a FUNC[TION] without a return spec.

But I think this does align with the reality that people expect FUNCTION to offer a return, out of the box...even with no return spec.

Guess this kills the idea that you have to put RETURN: in the spec to get a return. But I'm still not at ease with the idea of "hiding" a secret call to RETURN under the hood of functions to get type checking.

I Hate To Say It, But... :man_facepalming: Let's Do What JavaScript Does

In JavaScript you don't have to put a RETURN in a function. But if you don't use RETURN, the result is undefined.

We'd do the same, except with "none" (the ~ isotope, value used to mark unset variables):

>> scrambledrop: func [str] [take reverse str]

>> str: "Test"

>> scrambledrop str

>> str
== "seT"

Because there was no RETURN, the SCRAMBLEDROP gave back nothing... e.g. not the #T that came back from the take. But it didn't error either.

JavaScript gives the alternative of lambda functions, which evaluate to the body with an implicit RETURN.

We would do the same. Except their lambdas have a return (you don't have to use it)...but our lambdas actually wouldn't have a RETURN of their own as an option, they'd only have a RETURN from any enclosing functions:

  >> increment: lambda [x] [x + 1]

  >> increment 1019
  == 1020

  >> decrement: x -> [x - 1]

  >> decrement 305
  == 304

(Not having their own RETURN is important for lambdas used as branches of conditionals today.)

For the Near Term, I'll Make FUNC with no RETURN Raise an Error

Since people have been depending on the result "falling out", we'll have an intermediate period where no RETURN will just error.

This is a good time to go through and shore up your RETURN: specs with types, because one thing RETURN gets you is typechecking. (I explain above why RETURN is the locus of typechecking, and in the implementation it will be the only place for typechecking.)

And if you feel that you don't want or need a RETURN, go ahead and embrace LAMBDA. If you really are writing something quick and dirty where all the hassle of a spec is more than you want... why not go all the way with the arrow?

increment: function [return: [integer!], x [integer!]] [  ; what a hassle!
    return x + 1
]

increment: function [x] [  ; at least there's no verbose type specs
    return x + 1
]

increment: func [x] [  ; hey, an abbreviation
    return x + 1
]

increment: lambda [x] [  ; lost the abbreviation but at least no RETURN
    x + 1
]

increment: x -> [x + 1]  ; behold, laziness Nirvana!

Once Again, You Are The Arbiter of Your Experience...

Because I want to emulate Rebol2 and Red, I'm going to always look to make sure there are ways for everyone to get what they want. And when you can take module isolation for granted as working properly... you should still be able to interoperate with other codebases--even if you pick something fundamentally different.

So if you wind up wanting it, you can say:

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

With this, you'll get these semantics:

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

>> bar true
== 304

>> bar false
== 1020  ; the injected RETURN ran behind the scenes

@gchiu seemed to think that was sensible.

But I like things being less sneaky, I'd be more in favor of:

func: adapt :func [
    any [
         find spec [return: <none>]
         find spec [return: <void>]
    ] else [
        body: compose [
            (as group! body)
            fail "HEY YOU FORGOT YOUR RETURN!"
        ]
    ]
]

But this is where the adaptability is the great strength. I make the core coherent and work. Then you configure it how you like.

I think not erroring--but not letting a result drop out--is a good compromise. It lets people who want to write "procedure"-like things do so without being forced to put in a RETURN they don't want.

1 Like

I Want Core to Reserve RETURN-with-no-args for RETURN VOID

Things have evolved significantly, so I don't think I want to do this anymore:

something: func [
    return: <none>
    value [text!]
][
    if value = "" [return]  ; this *used* to act like `return ~none~`
    append data value

    ; expectation had been that this would give ~none~
]

While I know the return: <none> is up at the top of the function, it makes it clear in longer functions if your RETURN statement says what you're returning. return none isn't that much typing, and return ~ is even less. Both make it clear that you're not returning void.

(It's a bit funny, because at first I was suspicious of RETURN taking no arguments being a way of saying VOID. Even though it had cool chaining properties for void functions, I thought we'd do multi-returns as a variadic with return value1 value2...and that would leave no way to return voids and additional secondary values. But everything is different now, and RETURN with no argument being void is fundamental.)

I'd Discourage Casually Specializing RETURN to take No Value

If someone wants to specialize RETURN with a value so it takes no arguments and call that RETURN, I guess that's up to them...but... I wouldn't (any more). I'd either give it a new name, or have it still take a parameter just to say "I'm returning a value...not a void...but it's something predetermined, or calculated elsewhere".

 foo: func [x y z] [
     return: adapt :return [
          assert [value = <calculate!>]
          value: (something x) * y + (something-else z)
     ]
     ...
     return <calculate!>
 ]

(Of course, the more interesting examples are where how it behaves depends on what you pass it. But my point is just about thinking that RETURN with no parameters has a very specific meaning now.)

return <none> in Spec Only Hooked on RETURN

There's now an option for the lazy when it comes to "procedure-like things" that RETURN none. Just don't have a return spec, and don't use a return statement.

But once you've decorated the spec with saying you return: <none>, you've joined the non-lazy league. You might as well go ahead and put a return on all paths, to get hookability.

To recap:

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

I'm not all that concerned about what happens to lazy-league people if they write:

something: func [
    value [text!]
][
    return: adapt :return [print "I AM RETURNING!"]
    if value = "" [return none]
    append data value
]

You didn't call RETURN, so you don't get a call to RETURN for the bottom drop-out... unless you have decided to do this:

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

And if you do such things, then it's all up to you and your usage scenarios. I don't really even think this needs to be an error if you put a RETURN: annotation on...but if you want to get a hooked RETURN to run on all paths you need to actually invoke the RETURN.

Avoiding Weaving Tangled Webs of Deception in the Core :spider_web:

It goes beyond just aesthetics to not have "tricky" things (like the hidden RETURN). It also makes the parts align correctly. So if there are going to be weird alignments I'd rather people do that themselves...or have them use Redbol or some other "skin". Because there are more things that this is supposed to be able to do well than emulate Rebol2.