Why Isn't PRINT Invisible ("void")?

2022 UPDATE: Modern thinking in Ren-C is that void states only vanish in "interstitial" slots. They can be recorded in variables, just not be put in blocks:

   >> x: void
   ; void

The question of how permissive to be is still open, and at time of writing it does this:

  >> x
  ; void

  >> 1 + 2 x
  == 3

This impacted some elements of the discussion, so I edited the post accordingly to bring it up to date.


Since PRINT doesn't have an interesting return value, we might ask what the harm would be in making it invisible, vs give back something that trips up things like ANY and ALL.

But let's generalize the question to SOME-FUNCTION where the key point is that at the time you write it, you haven't thought of a meaningful result for it.

Limiting Interface Flexibility

If at the time of writing a function you know that it doesn't have a meaningful return value, then making it void--instead of returning a trash value--ties your hands in changing it.

People will start writing things like:

all [
    ...
    some-function ...  ; user assumes no effect, because invisible
    ...
]

But if SOME-FUNCTION had returned a trash value, then they could have gotten the same effect more obviously with:

all [
    ...
    elide some-function ...
    ...
]

This also gives more freedom to change the interface later, if you think of an interesting value to return. You can progressively add more return types after the fact. But once people assume you always return void, this trap will happen...you're locked in forever in a way that was pretty much completely avoidable.

In PRINT's Case, a Differentiated NULL Output Has Value

Having a return value from PRINT that is either an ornery value or nothing lets you offer the neat option of returning NULL if the result of the print was nothing.

 >> line: "some text"
 >> (print line) then [print "We had output!"]
 some text
 We had output!

 >> line: null
 >> (print maybe line) then [print "We had output!"]
 ; null

To do this kind of thing requires having an output value that wouldn't trigger THEN when there is output. So not null, and not void. The none isotope works for this.

I might be convinced that how people want to use PRINT is universally enough that they would rather it be invisible. But that would involve a very specific understanding of a very common function...similar to how elide and comment and assert and -- are known to have no result.

I don't think the average "no meaningful result" function fits in this category, and I'd say I'm fairly skeptical if PRINT belongs in it.

I Think "Void" Functions Should Be Used Sparingly

I think the feature should be discoverable, because how it's done is unique...and we are giving it a more normal name ("void function")

And it's all right that these forms of RETURN generate void functions:

f1: func [x] [return void]
f2: func [x] [return comment x]
f3: func [x] [return ()]
f4: func [x] [return (void)]
f5: func [x] [return (comment x)]

But this arity-0 return should not be a void function:

f6: func [x] [return]

Instead it should be either an error, or act like return none (e.g. return ~[]~, an isotopic block representing an empty parameter pack). While the latter sounds neat, it creates problems if people depend on it and start thinking they can throw a plain return in on its own line and not bother commenting things out after it... but the return picks it up:

Line Continuation and Arity Bugs: Thoughts?

So it's something that should not be allowed, or that has mitigation for this "return put on its own line in the middle of code" vulnerability.

Note: This pertinent question moved from a topic culled by curation.

(The topic was "should you be able to assign ~unset~ isotopes via SET-WORD! without getting an error, and the answer is obviously yes in light of new features...so the old conversation was just a jumble of outdated terminology which would confuse anyone finding it.)

On a tangential note: I'm not sure why PRINT returns a ~none~ isotope, and wonder if it wouldn't be better to have it be silent like ELIDE? Would that create havoc if new users did confuse its usage?

The usefulness of the return result is that it isn't always a none (~[]~) isotope, but NULL if nothing--not even a newline--was printed. This lets you use it with the likes of ELSE.

>> label: null
>> message: null

>> block: compose [((label)) ((message))]
== []  ; if you print this it outputs nothing--not even a newline

>> print block else [
       print "Here you can handle that there was no output."
   ]

>> if didn't print block [
       print "DID and DIDN'T are pure NULL-reactive if you don't like THEN/ELSE"
   ]

(Note: Performance dictates that returning the aggregated printed string itself is probably wasteful, since it is used so infrequently.)

Being able to elide print [...] covers the case where you want an invisible usage somewhere in the middle of an evaluation, which is nice and general...and not so strange once you get used to using it.

This is worth revisiting, since mechanics have changed significantly with voids

Let's pretend PRINT returned void for a second:

 >> print "voids are currently shown by the console (good info!)"
 ; void

 >> 1 + 2 print "but they vanish in interstitial slots"
 == 3

>> void? print "Targeting argument, won't vanish"
== ~true~  ; isotope

If PRINT returned void in its average success case, then an ELSE would treat the void as not having run... same as NULL. So the distinction wouldn't work.

>> item1: null, item2: null

>> print [maybe item1 maybe item2] else [print "No output!"]
No output!

>> item1: "Something!", item2: null

>> print [maybe item2 maybe item2] else [print "No output!"]
No output!

No Console Feedback Desired

One thing about voids is that you get feedback about them in the console that you don't get with the "none" indicator:

>> print "if this makes void, the console tells you"
; void

That's kind of annoying. So we must ask the question: Why doesn't the console pick the void state--the one that has no reifying value at all--as the true emptiness that it does not print?

That sounds nice in theory. But in practice, you wind up with this kind of thing:

>> 1 + 2 print "Hi"
Hi
== 3

You might think you could invent something that erases that product, maybe it's what a vertical bar would do:

>> 1 + 2 | print "Hi"
Hi

But... how would you write it? It either returns something (which makes it print out that thing) or it returns void (and has no effect, letting the previous evaluation seep through).

This is kind of the proof case for why the signal to the console to not display a result has to be "a thing". If it's not a thing, then the intent can't be expressed persistently.

Now that "none" is simply the ~[]~ isotope, we have a slightly greater justification on why it doesn't print.