@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:
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!