Why does FUNC require you to call RETURN?

Historically in all Rebols you could just allow a function's result to drop out of the bottom:

rebol2>> add-twenty: func [x] [x + 20]

rebol2>> add-twenty 1000
== 1020

In Ren-C you get nothing (well, "TRASH!", that doesn't display in the console)

>> add-twenty: func [x] [x + 20]

>> add-twenty 1000

You have to use RETURN if you want to return the result:

>> add-twenty: func [x] [return x + 20]

>> add-twenty 1000
== 1020

I see it can be worked around, but doing so is harder than a casual Reboler might think:

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

While you can do that, it seems that it would be more efficient to just include a way to drop results out in the box.

Maybe FUNCTION could require a RETURN, but FUNC does not?

For those curious about the true history, Carl was conflicted on the matter:

http://www.rebol.net/r3blogs/0025.html - "Leaky Functions"


Use LAMBDA.

>> add-twenty: lambda [x] [x + 20]

>> add-twenty 1000
== 1020

Things to know about LAMBDA:

  • It doesn't have a definitional RETURN at all. So if you execute a RETURN inside a lambda and it's not an error, that means it found someone else's return to run:

    F: func [x] [
        L: lambda [] [return x + 4]  ; runs F's return
        L
        panic "unreachable"
    ]
    
    >> F 300
    == 304
    
  • If you want to annotate LAMBDA for what it returns, the syntax is []: [<typespec>].

    add-twenty: lambda [
        "Add twenty to the passed in integer."
        []: [integer!]
        x [integer!]
    ][
        x + 20
    ]
    

    It does look a little bit unusual. But it was chosen after a long amount of reflection. The empty block shows that there's no RETURN: or YIELD: or other word in the frame that you can use to trigger a return...and it's especially good to reinforce that when you consider the effect I show above of "RETURN means someone else's RETURN".

    Perhaps you'll find it jarring at first, but give it a chance. It dovetails rather nicely with the "transparent" evaluator behavior of []:

    >> []: 10
    == 10
    
    >> []: pack [10 20]
    == \~['10 '20]~\  ; antiform (pack!)
    
    >> error? []: fail "HI"
    == \~okay~\  ; antiform (keyword!)
    
  • If you don't have an interesting result, and don't want to document that by writing FUNC with return: [trash!] you can use PROCEDURE (shorthanded as PROC):

    >> data: []
    
    >> P: proc [msg] [append data msg]
    
    >> P "result of P will be squashed"  ; no appended data returned
    
    >> data
    == ["result of P will be squashed"]
    

    PROCEDURE offers a RETURN. And to make generic macros or patterns easier you can pass it an argument... but if you pass it an argument it will be typechecked as TRASH!


The fact that the specific instances of the function argument variables are not available until each function instantiation is run makes this trickier than it first appears. Binding composition is just hard to begin with, and it gets harder when the things you're binding to don't exist yet at the time of composition.

But I find it encouraging that there are answers to binding problems in Ren-C (vs. just throwing up our hands and going "well, can't be done", which is pretty typical in other Redbols.)

So I'll mention what actually happens when you reach the end of a FUNCTION without calling something that calls the definitional RETURN* function (or otherwise THROWs).

the definitional RETURN*--if it is an ACTION!--is executed and passed -no- arguments.

Since the default behavior of RETURN* with no arguments is to give back a TRASH!, that's what you get.

You can override this, and give specific behavior to the case where no argument is passed to RETURN* (e.g. a "hole"):

>> foo: func [x] [
       return*: adapt return*/ [if hole? $value [value: <small>]]
       if x > 1000 [return <big>]
   ]
        
>> foo 1020
== <big>

>> foo 304
== <small>

Do there really need to be two different generators?

Couldn't FUNC(TION) act like a LAMBDA by default (or if you use a []: return spec), but then get a definitional return if you put return: in the spec?

Maybe []: [] could also be a signal to act like PROC (no RETURN, and no result, so force to nothing).

foo: func [...] [...] 
; result drops out, no type check, no return

foo: func [[]: [<spec>] ...] [...]
; result drops out, typecheck, no return

foo: func [return: [<spec>] ...] [... return ...]
; panic if end reached without return, definitional return and type check

foo: func [[]: [] ...] [...]
; return trash when end reached, no return

It would be possible, but it doesn't have a smooth "you get what you pay for" scale.

With FUNC as a distinct generator from LAMBDA, you're able to get a construct that offers you the ability to return any value with just

 func [...] [... return ...]

Whereas if you had to ask for it, that would add boilerplate:

 func [return: [any-value?] ...] [... return ...]

The goal is to avoid forcing you to put a type spec on if you don't have anything to say about the types yet.

This is not to say that auto-detecting generators shouldn't exist. In fact, I think that COMBINATOR might do exactly what you propose. So instead of there being LAMBDA-COMBINATOR and FUNC-COMBINATOR, cue it based on the presence of return: [...] or []: [...] (probably pick lambda semantics by default if neither present).

So maybe this case could probably work.

foo: func [[]: [<spec>] ...] [...]
; result drops out, typecheck, no return

That would make it easier on anyone who wanted to write an auto-detecting generator.