What Are Definitional Returns?

Novices using Rebol2 or Red aren't really clear on how their RETURN works. (Or doesn't work, as the case may be.) RETURN climbs the stack until it finds a function that is willing to accept returns.

  • Functions in Rebol2/Red that won't accept returns: IF, WHILE, or pretty much any native
  • Functions in Rebol2/Red that will accept returns: any user FUNC you write

To give a brutally simple example, you cannot implement UNLESS in terms of IF:

 rebol2>> foo: func [x] [if not x = 10 [return "not 10"] return "it's 10!"]
 rebol2>> foo 20
 == "not 10"

 rebol2>> unless: func [cond block] [if not cond block]
 rebol2>> bar: func [x] [unless x = 10 [return "not 10"] return "it's 10!"]
 rebol2>> bar 20
 == "it's 10"  ; D'oh

That UNLESS, because it's a FUNC and not a native, is a candidate for receiving RETURN. So the UNLESS itself returned "not 10" instead of returning from bar. Execution continued and the `return "it's 10!" ran. I maintain that correct behavior constitutes another must-have, and I was by no means alone in this, nor the first to say so.

>> unless: lambda [cond block] [if not cond (block)]  ; see note re: group!
>> bar: function [x] [unless x = 10 [return "not 10"] return "it's 10!"]
>> bar 20
== "not 10"

I do not consider annotating UNLESS to say "I'm the kind of thing that doesn't catch returns" to be remotely acceptable. I'd sooner throw out the project than go that route. Addressing definitional returns wasn't at all trivial...even though conceptually it was understood what needed to be done. It was one of the first things I tried to do in open-sourced R3-Alpha. The rearranging I had to do in order to understand the code well enough to accomplish it laid the groundwork for many features to come.

(Note: The reason you have to put a group! around block is due to soft-quoted branching, and I argue for the tradeoff here.)

Worth noting is there's an issue in the Red repository for this as well:

Says @hiiamboris:

"Maybe crazy: bind every return & exit the same way we do bind function arguments/locals, so that they know their scope from the context they are bound to. But then if we write code: [return 1] f: does [do code] it won't work. Trading one limitation for another, plus likely more complexity in interpreter code. And we can't bind break/continue as there is no context for loops..."

"That’s not extremism; that’s epistemic hygiene."

I'll point out that in Ren-C you don't trade one limitation for another, because binding propagates from tips of lists downward in evaluation, giving unbound material meaning as it descends. So unless you explicitly bound a RETURN word in a block, it will get its meaning from context, even if definitional.

I think that making return of trash as simple as return ~ avoids needing a separate EXIT definition. Also, it's nice to retake the word EXIT for other things.

And of course, Ren-C has definitional BREAK and CONTINUE (as well as definitional THROW, where you can also pass CATCH the word to use as its "throw")

So not crazy at all, if you have the right binding ideas.

"plus likely more complexity in interpreter code."

Simple but broken is not actually simple. It's just...broken.

"everything should be made as simple as possible, but no simpler"

1 Like