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: [~] be an Exception?

The point of introducing this 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: [~]
    value [text!]
][
    if value = "" [return]  ; this RETURN acts as `return ~`
    append data value

    ; expectation has been that this would also yield ~
]

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: [~]
    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

Time To Axe No-Argument RETURN

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

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

    ; expectation had been that this would give trash (~ isotope)
]

While I know the return: [~] 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 trash isn't that much typing, and return ~ is even less.

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...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: [~] in Spec Only Hooked on RETURN

There's now an option for the lazy when it comes to "procedure-like things" that RETURN trash. 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: [~], 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: [~]
    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
]

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 ~]
    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.

So I actually tried to walk the talk here, and went around changing some functions into lambdas if I thought returning the body result was sensible.

To help make that work in more cases, I actually made LAMBDA able to take spec blocks:

x -> [print ["I am a lambda x + 20"], x + 20]

lambda [x [integer!] "number to add to"] [  ; look, a spec block!
    print "I am a lambda x + 20"
    x + 20  ; look, the result just falls out
]

...but since they didn't have RETURN, that meant there was no type checking on the return result. This made me feel a bit hesitant to make these transformations, unless the function really could return anything and so the return annotation was superfluous.

I Think This Was A Mistake...

Part of what I've been trying to do is to build up features from the ground, in a way that someone with different ideas could build them differently.

This means "reskinning" language constructs to where even foundational things like type checking are reimagined by a creative user.

So I think that having LAMBDA be the baseline of something that doesn't have typechecking on its arguments--or its returns--is a useful touchstone.

This makes it a bit of an "homage to lambda calculus" by having it be the most basic sort of function, and building all the other stuff on top of it.

Hence it won't have RETURN, multi-return, type checking of arguments, or anything. Just the most basic of something that takes arguments and returns what its body evaluates to.

So Pretend I Didn't Suggest LAMBDA as a FUNC Alternative...

If you disagree with my belief that a FUNC(TION) that doesn't have a RETURN doesn't return a useful value... then you need to redefine FUNC. LAMBDA is being stripped back...to where it's more basic than you want!

If you were to build your own FUNCTION with its own RETURN, you would build that on LAMBDA.

But LAMBDA is the low-overhead choice you want for branches:

 case [...] then branch-result -> [print ["use to process" mold branch-result]]

 case [...] then lambda [branch-result] [print ["non-enfix" mold branch-result]]

Think of them more as low-level, like BLOCK!...

2 Likes

Ugh... as it turns out... I might not have a choice but to allow a lambda variant with spec blocks.

Because without it, I don't know how to make a function which does type checking but doesn't have a RETURN. Sometimes you need those.

...and I can't think of a better name for a function without a return than LAMBDA. All the ways of annotating a function to say it doesn't have a return kind of suck.

Case in point that is triggering my backtracking: what about QUIT in Redbol? It had a refinement called /RETURN. How do you declare a function for that?

  quit: func [<no-return> /return [<opt> any-value!]] [...] 

Yuck. And it actually does have a "return"...it's just a refinement called return.

The problem is that if you don't think of it as using something lower-level than FUNC, you wind up talking about the thing you want to take away. I think that's destined to come off badly.

And as this thread says...I've said that you need a RETURN for a FUNC to return any value at all...a point of view I'm committed to now. So a FUNC with "no-return" really couldn't return anything!

So LAMBDA Probably Has To Be It

We'll now enforce that all FUNC(TION) have RETURN. (Previously it was possible that if you named a local or refinement RETURN then it would disable the definitional return creation. That's getting dropped now, you'll just get an error.)

If you don't want a RETURN pre-defined, use LAMBDA...and you can then choose to have arguments called RETURN if you want. But if you use a "non-trivial" spec block you'll get type checking on that lambda.

Hence I guess some more primordial building block, let's say LAMBDA*, is the form that truly lacks the type checking? :grimacing:

It's an imperfect art, but I do think it gets better day by day.

2 Likes