The RETURN of NONE (?)

@rgchris has convinced me, and I am now committed to the idea that "VOID" is not a datatype, but the characterization of what were previously called "invisible" functions.

There is a nice sense in which this lines up with traditional "void" functions in C (and related languages). Because absolutely no value--not even some dummy or placeholder--is returned. It means the libRebol C API function signature void rebElide(...) is consistent with the ELIDE construct signature...they both specify their voidness in their return spec.

But I've written up in a separate post why I don't believe things should too casually be void functions:

The go-to for creating a function without a meaningful return result should be a reified meaningless result...because full-on vaporization is a contract which is difficult to turn back, and is not easy to sense from a callsite (e.g. inside an ALL [])

Relying on a generic tool like ELIDE gives you clarity at callsites of what you're doing, and means you don't have to work "I elide my result" into the name of your function somehow. Returning a meaningless value also permits interop with ELSE and THEN if you decide you want NULL-reactivity.

Maybe PRINT is one of those things that should be an exception (along with assert and -- and breakpoint)...but it would be a rare case.

What Should We Call The Meaningless Result?

The BAD-WORD! of ~void~ is now ruled out as "the meaningless ornery result". Because we are saying void functions don't return any value, not even meaningless ones. Void is retaken for the new and more familiar term for characterizing invisibles.

I have been going with ~none~ as the brief choice. I'd think that ~nothing~ might be a bit better--and not have the confusion with legacy. But it was taken for a reason: it's short. Considering it off-limits would sacrifice an easy thing to type. It would appear a lot of places, e.g. functions that want to automatically discard results of their bodies would wind up as return: <nothing> or similar, etc. It's verbose for arguably little gain.

What it has in common with historical Rebol's NONE is that it is a reified concept of nothing. What it does not have in common is that it is neither true nor false, and errors in conditional logic...like all the other BAD-WORD!s. So it's just another kind of unset.

Type signatures with BAD-WORD! in them are a bit weird:

print: func [
    return: [<opt> bad-word!]
    ...
 ][...]

So having a type constraint for just the ~none~ as none! may be worth it. This doesn't make it a distinct data type, it just narrows down the specifics of it.

print: func [
    return: [<opt> none!]
    ...
 ][...]

Alternatives: ~trash~, ~bad~ ...?

Outside of its collisions with Rebol2, the word NONE is a bit close to NULL. (And you'd have people asking what the difference was between "null" and "nothing", if that was chosen, the longer name probably wouldn't help other than being more obviously distinct.)

We're looking for a short word here that walks the line between "alarming" and "nothing to see here", that's still short. This is why I'd thought ~void~ was that word...it ticked the boxes, but I've been convinced the usage was confusing.

For a little while we had the lone ~ which was nameless (which made it a good candidate for being something the console didn't display). But its temptingness as an operator and hard-to-see nature made it too short for something that should trip your awareness in a block the way a BAD-WORD! should.

The problem I see with things like ~bad~ is that they make the interface of the function look more alarming than it needs to (return: <bad>), and it also doesn't seem very appropriate for the console to be hiding something called "bad" from you.

Retraining Usage Of Arity-0 RETURN

I'm warming up to the idea of func [] [return] being invisible. One aspect of warming up to it is the idea that if we are going all the way and calling them "void functions" then this kind of lines up with being that.

So I'm also potentially warming up to the idea that func [x] [comment x] could act like COMMENT. If we said that func [x] [] returned NULL that wouldn't work, because we'd be pre-seeding with a NULL and hence func [x] [comment x] would also be NULL, since the comment wouldn't change the output.

(I don't know if we say that's just life, since if true [comment "hi"] is not invisible, and hence we chalk up the invisibility magic to something only RETURN can do, due to its special relationship with FUNC. Which seems a legitimate philosophy to have. Why worry if func [x] [comment x] is NULL if you can get what you intend with func [x] [return comment x] ?)

UPDATE: Thinking this through further I realized that it's not just multiple returns that would get problems caused with this, but also null isotopes. We really do need a way to say "return as-is", so we might just be stuck with the return @(comment "hi") form if you want to get a verbatim chain that includes invisibility and isotope status.

This brings us to the point: If we embrace the idea of RETURN with no argument as the "weird edge case for facilitating void functions" then we have to drive in the non-weird way to handle the basic case of returning a meaningless result. People have to look at RETURN with no argument and go "hey, wait, why doesn't that return have an argument" and have it pop off the page as an oddity, worthy of a potentially surprising behavior.

return '~none~ is a pain to type. I'd been wanting to avoid functions that returned BAD-WORD!s to avoid the sorta confusion situation of :action = type of none ... but if making it an action is what we have to do in order to get the syntax of return none then that is what it is.

Sidenote: Inheriting Return Types

I'm talking about finessing the question of how to do a chained RETURN on a function that you don't know if it's going to vaporize completely or return a value. That's something that bothers me about C, because it doesn't let you write chaining void returns:

void my_function() {
    return some_void_function(...);  /* this is not legal! */
}

That annoys me, because it makes it hard to write generic code that doesn't want the bad properties of a macro (repeating evaluation of arguments if used multiple times, etc.)...but throws a wrench in being able to abstract across return values.

Yet then there's that other question of how to label your return type. In Ren-C we have the option of just not putting a type signature on it, but what you often want is the signature of what you're wrapping. I guess we could do this via COMPOSE on the spec:

my-function: func compose [
    return: (return-of :some-other-function)
    ...
][
    ...
    return some-other-function ...
]

Anyway, I just wanted to mention that we're still pretty far from utopia in this medium, even if we get the chaining part itself right!

There's one twist that could make it more palatable to use NULL as the "meaningless" result, e.g. return: <null>.

That twist is null isotopes. Because it means a function like PRINT could even discern the "I printed something" case from the "I didn't" and still have that be null-based.

A plus side of this is that if you want the console to hide things from you, then NULL seems the obvious choice as it has no representation. :-/

But I think it's bad to lose the noisier errors at callsites. Your intent is meaninglessness, and unfortunately NULL just carries too much meaning in the system. That meaning ranges from opting out of refinements to silently opting out from appends, ceasing chains of ALL [...], etc.

This is a hard slot to find a word for. We want something with overtones of "ok" and "invisible"... almost more like return '~done~... to say, "yes I ran and I finished, and that's all you need to know".

foo: func [return: <done>, ...] [
    return done
]

I guess then if the console suppresses showing ~done~ it could be justified as "well it was done, no need to show you that, you got back to a console prompt with no error raised".

Or we could just give up and say it's ~unset~, make unset an ACTION! that returns ~unset~, and say it's unsets that don't print:

foo: func [return: <unset>, ...] [
    return unset
]

For fans of history, this might be the most palatable option. We need a clean way to unset variables anyhow:

unset 'x  ; never liked this, puts thing you're assigning on the right

x: '~unset~  ; hard to type

x: unset  ; that's not so bad

But while I've embraced the "BAD-WORD! that is labeled ~unset~", this puts us right up against having a datatype constraint for that which under present schemes would look like UNSET!, e.g. print: func [return: [<opt> unset!]] [...]. I'd been trying to get away from that.

That's bad thing #1, maybe it's not that bad.

But bad thing #2 is that I don't think it's very instructive in the console to have ~unset~ be invisible in the output:

>> get/any 'asdfdadsfasdf

>>   ; hey, wait a minute.  Why no output?

It's better in my view to see it:

>> get/any 'asdfdadsfasdf
== ~unset~

I can see why you wouldn't want to see this:

>> print "We tried this, and it was annoying"
We tried this, and it was annoying
== ~whatever~

But I don't feel the same argument applies to suppressing the visibility of things that are unset.

Of course, one way or another, you're declaring some value as being such that the console doesn't show it to you. Brilliant ideas welcome on how to keep improving this.

That's the Long Story Long...Any Better Ideas Than ~none~ ?

The only candidate I see that seems to be viable would be ~done~... or maybe going back to the ~unset~ concept if we just want to say screw it and it's not worse than Rebol2.

The other question--and maybe one only @rgchris can answer--is if the VOID! and ~void~ term actually is more palatable than he thought... that BAD-WORD! and other choices are more distracting... and I was right the first time around (or if he has a better systemic solution).

I'd still lean toward my own sense of what I'd consider (or feel) my primary definitions of NULL and VOID—where NULL is the product of nothing and VOID is the realm of emptiness—a place with no beginning or end, or indeed no definition (void as in voided checks is not the first meaning that comes to mind).

If I were to expand on that, I'd say that NULL is negative nothingness and BLANK is positive nothingness where we must affirm across the wire that nothingness is the intent.

If I were to indulge a little further and incorporate bad words, then VOID becomes more like outer space and the various defined bad words are stars within distant constellations.

I think this is why the idea of VOID as replacing UNSET was key—there's an infinite array of words out there and until they are defined, they are just points in an endless vacuous universe. Language can be fluid and subjective, this is where my mind went.

All right, like I said...I can go along with this.

But there are still some of the aesthetic issues surrounding BAD-WORD! and how to get function interfaces to not look too... bad.

return: <void> felt pretty communicative and bland (as "no interesting result"). But I really don't think it's a good idea to be making all functions with "no interesting result" behave like ELIDE, for reasons I've outlined.

I also do not think NULL is good either. It has too much meaning to serve a meaningless role.

So we need a new return: <...> and an associated ~...~ it gives back. I feel a distinct BAD-WORD! from that used for an unset variable is fitting, given the ability to give these different labels.

But, like I say, maybe we give up and it's return: <unset> with ~unset~. It kind of feels like it makes more sense now than it did before.

I still am reluctant to call the data type itself UNSET!. I prefer type of first [~unset~] to come back with something that is more concretely representative of what you're dealing with. And I also think the ability to change these labels to be more communicative...to any symbol a word could hold...is good. This makes BAD-WORD! seem fitting. It's just a bit jarring to see it in typesets.


The issue of where return of ~unset~ for meaninglessness breaks down is that we're talking about that particular label having meaning of "unspecialized". So if you say:

>> f: make frame! :append
== make frame! [
    return: '~unset~
    series: '~unset~
    value: '~unset~
    part: '~unset~
    only: '~unset~
    dup: '~unset~
    line: '~unset~
]

>> f/series: [a b c]
>> f/value: 'd
>> f/dup: duplicator-method x y z

What if duplicator-method thinks of itself as having a "meaningless" result, and says return: <unset> If the goal was to receive errors in such cases you wouldn't get one. (I'm proposing that any BAD-WORD! besides ~unset~ would be an error in this situation, unless you explicitly HIDE the field, in which case it is presumed specialized)

I think the general intent is to poison variables with a "you didn't mean to save this value", as opposed to mimic the illusion of unsetness. And that's one of the big opportunities of having labeled bad words. So I think we'd waste that by using unset here.

I also think that return: <unset> looks kind of confusing, and people would prefer to see return: <void> in these cases. But that's not good under the elide-definition of void to be running to as a default.

And as I've already mentioned, I don't know that having the ~unset~ disappear in the console is good for learnability.

So, this leads back to my return: <none> and ~none~ suggestion. But while that looks nice, it's only one step away from return: [bad-word! integer!] or return: [<opt> bad-word!] which feels a bit more alarming than is intended...while at the same time, being pretty accurate.

A more benign type name like none! would gloss over that. Saying that ~unset~ and ~none~ are both instances of type NONE! is a bit weird. It would make more sense if NONE! was a type constraint... e.g. a BAD-WORD! whose spelling is ~none~.

Anyway, I'm repeating myself. But this is the important part to pin down.