How Attached are we to Functions Return Last Result?

UPDATE: 2021: Use LAMBDA if you want to return the last result; FUNC(TION) requires a RETURN statement and will return trash otherwise. Full rationale:

Implicit Execution of RETURN in functions = ...BAD (?)


In JavaScript, the failure to specify a RETURN statement is basically the way of saying you don't return anything. Functions don't accidentally leak values, you just get undefined:

> function nada() { 1 + 2; }

> nada()
<- undefined

> function three() { return 1 + 2; }

> three()
3

I know this isn't how Rebol has historically operated. But the historical operation of Rebol can lead to the accidental leakage of a lot of values which the caller wasn't necessarily meant to see. There's currently a burden to annotate in the spec when a function doesn't return something.

So in the "Let's not give JavaScript any upper hand" line of thinking, should this be re-evaluated? I must admit I kind of feel their way is clearer. :-/

1 Like

If return comes, make it optional. The current behaviour is fine by me. Adding return is undoing part of REBOL way, REBOL takes care of taking and returning the last result. If you like it or not. It is not the end-user but the programmer that has to deal with it and the programmer should be able to handle or use PHP or Java instead.
Having the function return without declaring variables or having to specify a RETURN statement is very useful in quicky programming.

This could also be another construct. MACRO perhaps, or INLINE...where the idea is to create something that substitutes an expression.

>> foo-macro: macro [x y] [append x y]
>> foo-macro [a b c] 'd
== [a b c d]

>> foo-function: function [x y] [append x y]
>> foo-function
; no result (e.g. void!)

Or something. I dunno, I think it's worth asking if it's a great idea to leak by default. I personally like the RETURN making it clear whether something is actually meant to be a return result...it can be hard to tell when looking at:

foo: function [...] [
    ...
    ...
    something-or-another
]

Is that intentionally returning the result of something-or-another? Or is that a function intended to have a side effect, whose result is not part of the contract?

1 Like

Very.

To me it is part of what defines Rebol. Every function which doesn't return anything disturbs the flow for me, and breaks the ability to chain functions together.
It is part of the programmers responsibility to return something sensible.
(Well, this is my opinion at least.)

1 Like

I have to read and maintain a lot of code (including things like the wild Rebmake), as well as even odd things I've written. I really find that it can be bewildering on a function that has no RETURN: documentation and some weird expression in the last slot to know if that thing is used by any callers.

My opinion here is that having a return-or-not is a big leg up in understanding the reach of a function, and whether you can reorganize it (or add more code to the end) without breaking something.

But as I've outlined in Implicit Execution of Return in Functions, this goes further than just our preferences for readability. Because RETURN is a function itself, we're in different territory...and it's a territory I want people to be able to explore with doing things like hooking the RETURN function and customizing it.

There will be two options:

  • Use LAMBDA. Lambda is known in languages for a construct that does substitution of the results of a parameterized expression and doesn't need RETURN.

    • One feature of lambda is that it does not have RETURN of its own. This makes it useful for block branches and other services inside the context of another function.

    • If you decide at some point you do want a RETURN, you will have to promote your code to a FUNC(TION) and add a RETURN on all paths--including what drops out the bottom.

  • Go your own path and define FUNC to RETURN its body as a group:

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

    People should give deep thought to whether the toolbox of definitions they import into every project should override fundamental behaviors. But if you find something is a closely held belief, then do it.

I must admit that this makes some sense. So I hope this'll lead to more conscious decisions on what to return.

2 Likes

Yes, having function that have a none as closing statement, to make sure the function does not return something.

It's true that you could adopt a reverse convention, by which any function that doesn't have a meaningful return result ends just saying none.

But the problem with conventions is that not everyone follows them.

I'll maintain that the best answer here is for me to do what's "right" by the architecture and my senses, and encourage people to be as "wacky as they want to be" by making their own box-of-tools they import on whatever scripts they write.

And really, your script probably shouldn't have a lot of "FUNC" in them anyway. Each task should probably have you quickly dialecting a layer suited to the task, and you should quickly climb higher than FUNC.

I think this is the philosophy of the language--if you're not bending it, you're probably not really using it. Each script should have a setup which does the bends that suits the task, and anyone reading the script should take note of that being the context they're operating in. With module isolation working properly, this becomes practical for each and every script to rip things up arbitrarily.

My job is to make sure it can bend, and I'm doing a good job. :slight_smile:

2 Likes

Something crossed my mind as a thought.

Right now, the omission of a RETURN: in a function spec is something that indicates carelessness. Coding standards for any "good" project would mandate that one be put on. The absence of a return spec is a red flag suggesting that it hadn't been formalized yet.

We could spin it another way, and say that a function without a RETURN: in the spec represents a procedure. e.g. no return spec means a trash result.

This alternative to "no return spec means unconstrained result" has some appeal. It would mean the omission of a RETURN is something that can occur frequently in a "good" program, and that those good programs would be shorter.

But it would add a burden to off-the-cuff code. Yet that off-the-cuff code now has the burden of including a RETURN statement, so would the burden of saying return: [any-value?] be that much more? And I've spoken about shorthands for this e.g. as return: <any> in the past... might that make it better.

I guess I find the long-tail argument that "good code can be shorter" to be somewhat compelling.

2 Likes

Well, this seems to be the objectively best way forward.

1 Like

What is your best way forward option then?
I like that there is no need to add RETURNs at the end.That is refreshing. If you do not want to return a value, add NONE at the end. And even values that would be returned, if no other function was waiting for that info, it would be discarded anyway. And you can always choose to add a RETURN to make it clear what to return, or it returns a thing that was not last to be computed.
Adding a RETURN to distinguish between functions and procedures is just copying from other languages without a clear need to satisfy programmers who will never switch to Rebol anyway.

This is adequately covered by LAMBDA.

>> foo: lambda [x] [print "Use Lambda if you want fallout", x + 20]

>> foo 1000
Use Lambda if you want fallout
== 1020

The tradeoff is just that you don't have a RETURN defined in lambdas.

I've already explained that people can build their own variant of FUNC(TION) and have it do whatever they want. The stock function will return trash if there is no RETURN statement, so that is no longer negotiable.

Remaining open question is just the meaning of not having a RETURN: definition in the spec. I'm liking the concept that no spec means procedure, but haven't made the jump yet.


...although...there is the possibility of saying that if you don't put a RETURN: definition then there is no RETURN available, e.g. it acts as a LAMBDA, and just gives you the last result. Then if you add a RETURN: to the spec (even if just a return: [~]) then you must use it. This would address my concern about not executing a hooked return by omitting it.

I've also had some misgivings about whether LAMBDA should pick apart multi-returns vs. take normal arguments like a function. e.g.

>> if true [print "condition", pack [1 2]] then a/b -> [print ["branch" a + b]]
condition
branch 3

But there are also questions about if there is no RETURN statement, how would LAMBDAs specify return type checking.

I didn't realize it, but Carl had written a Rebol 3.0 Blog post which was speaking to the security issue of leaking values:

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

He says "Of course, we cannot change the default function return case (as shown in the first example) because it would break far to much code."

Reactions From The Comments

Brian Hawley predictably says that this could be driven by a setting.

Sunanda has the weird suggestion this be a feature of a function being in a module... since modules were new, any code in the modules could obey the new rule: "That does not need any additional syntax at all.....Simply document that a function in a module always return (say) NONE if it has no explict RETURN value."

Gregg points out that having FUNC behave differently in modules would be weird, and what if you composed the function across a module boundary.

@Brett has an interesting remark that is probably worth a post considering it here in the module category: "Re-reading module blog entry and this front line entry leaves me wondering if limiting of access to module words is enough. For super security should values (like block!, string! and issue!) be tied to a module too. If a non-exported value was about to be leaked it could converted to an error. Modules then are not just a namespace, but a memory space."

Jaime suggests a separate PROC function generator.

Oldes says break it, none of the code is going to be compatible anyway.

Funny Enough, I Agree With Oldes This Time

Ren-C actually did add PROC, but at a time when there was both FUNC and FUNCTION with different semantics, there was PROC and PROCEDURE with different semantics.

What ultimately soured me on the idea was that any time you created something "function-like", e.g. METH and METHOD, you face again the idea that you have to find some way to name the version that returns a value or doesn't. ("METHPROC" and "METHODPROCEDURE"?)

Ultimately FUNC and FUNCTION were unified as synonyms, making it even easier to make FUNC-like things that put all their behavior cues inside the spec block.

Anyway, it was interesting to see this discussion that I didn't know had happened. But Ren-C really clinches the issue since each function's localized RETURN can be hooked or replaced. You don't want values falling out the bottom of the function to be exempt from whatever finalization RETURN might be intended to do.