Another Lingering Idea: Named VOID!s

Something @rgchris had done that I liked was to use TAG! in module headers as a sort of TBD when the script header contained a configuration for that script. You could edit and go.

 Rebol [
     Title: "My Cool Script"
     Description: {
         Edit the variable below to set the directory.
     }
     Directory: <your-path-here>
]

The code looks to see if the directory is a FILE! or a TAG!. If it's a tag, it errors. But the erroring has to be done manually.

This made me wonder about what it would be like if VOID! was like a WORD!, in that it had some interned immutable spelling. It would error if you didn't replace it.

Just to pick a notation, let's say we took ~ for this:

     Directory: ~file!~

Being a VOID!, it would error if you tried to access it.

I've also thought that since VOID!s are a kind of deferred error--a bad result that's only there to be bad if you actually use it--that the text in the void could help guide you to what happened.

With branching structures, it's confusing that nulls are turned into voids if you don't use the @(...) branch forms. But wouldn't it be clearer if there was some name on that void?

 >> if true [null]
 == ~branched~

Or if you reduce some code that tries to put a null in a block but can't:

 >> reduce [<a> if false [<b>]]
 == [<a> ~nulled~]

And variables that didn't have any definition could have a nice name too:

 >> get/any 'asdfadsf
 == ~undefined~

Functions that had no value to return might go with ~nothing~ or ~no-return~.

But despite their different visual representations, they'd all be VOID!.

This feels like a strong play for Ren-C's hand. It makes the "brick" a lot more useful for building with, and you can already see above that it would guide people to better awareness with things like ~branched~.

3 Likes

I like this a lot, it increases transparency in a good way and should make the language easier to learn/understand. I don't see many downsides listed-- are we missing anything?

2 Likes

Consensus on a notation is probably all that's needed.

There's even be enough cell bits that voids could trace back to the file and line where they were created, if that were wired up. So if you're downstream of a void you could get information on where it came from.

(I was just thinking about what would happen if frame-local voids were initialized to ~local~, but then wondering about what would happen once it got far from home and you wondered about its origins...local to what?)

Similar ideas might be possible for BLANK! and NULL. I should tinker with it.

2 Likes

YESSS :+1:
I like it. Much.

2 Likes

Though I've had this thought for a couple of years now, I hadn't made a specific post about it where I showed the names (e.g. ~branched~). But in doing so, it kind of reinforces just how many of these voids you might see.

The rule for the console could be more narrow for what it does not show. One possibility would be the unique "empty void" is interpreted as "don't show in console". It means that for functions you didn't want to print a console result you'd just say [return ~] and that would be how you did it, with the baseline of functions printing for ~no-return~ (or whatever, I guess I'm feeling sort of attached to ~void~ for this purpose).

(Note; since voids are "ornery" you must quote them to not get an error when they are encountered in an evaluative contexts.)

 >> do []
 == ~empty~  ; probably better if this shows, vs being invisible with ~

 >> suppressed: func [x] [if x [print "not printed"] return ~]
 >> suppressed false  ; console knows not to print plain ~

 >> fallout: func [x] [if x [print "not printed"]]
 >> fallout false
 == ~branched~

>> data: []
>> proclike: func [return: <void> x] [append data x]
>> proclike 10
== ~void~  ; consider non-`~` voids shown by console a feature vs. bug

In any case, we are running out of symbols. And given the scant usage of ~ today and its undesirable visual nature... hard-to-hit-on-keyboard location... it seems clear that this would be a win over what tilde is used for in today's codebases.

The argument for the purpose like in headers for "TBD" seems a solid one for a light and usable notation...vs. some monstrosity like #[void! "branched"]. People will be looking at these a lot, and once they do they may become curious and start using them in expanded ways.

Potential for this becoming a frequently-reached-for light-error mechanism might be high. Conversion to error could use the void symbol as the ID (given that these are constrained to WORD! spelling rules and interning so multiple instances aren't taking up multiple copies of the string). Conversion of errors to voids could be another interesting direction. This gives you something like the "armed" and "disarmed" states of Rebol2, but a lot more interesting.

(Note: This suggests avoiding internal tildes, so that ~bad-mojo~ can be converted to bad-mojo without incurring the problem of being a void itself. I think this suggests disallowing internal ~, and only having it at the beginning and end.)

While Rebmu will mourn the loss of ~ for word names, it will give another tool for dialecting. So it won't be like it can't be used.

If the return result for void functions is just ~void~, which does kind of look better than ~no-return~, that makes me feel the name for the type should change so it's clear that void is for return results. We could call the type TILDE! although that doesn't really have the connotations for its toxic nature. TOXIC! ? I've made my many arguments over time about why it's not UNSET! and I'm more entrenched in that thinking than ever.

Maybe it's not so bad if ~void~ is the nominal void as used in function results, even though the type is VOID!. It makes me uncomfortable, but only a little uncomfortable...there's finite names, some things have to give. (Also: you can make a WORD! that's spelled word.) The whole thing seems learnable overall.

2 Likes

I should mention opportunistic invisibility. What's wrong with showing all voids, and using return @()?

Well... it might not be a good fit. Complications are:

  • If you typed something like 10 + 20 help foo you'd get the help for FOO, followed by == 30

  • Invisibility detection needs to be covered by DO. e.g. if your code to evaluate is [help foo] then you'd have to first turn that into a GROUP! (help foo) to signal disappearing is okay. Then you would need to have a return result from DO of that to distinguish from plain VOID!. (This will be needed anyway, just not necessarily for this.)

I just kind of feel like HELP isn't meant to vanish. It's not a comment. Something about it seems a better fit as an operation that returns a void you'd rather not see.

1 Like

One place where this could be used in a non-trivial way is in "NewPath" that wants to use the UNIX home directory.

path: '~/foo/bar

If we allow VOID! to be one of the things in paths, there could be a special exception that says that particular pattern is permitted by the TO FILE! conversions. You'd get into a situation with:

 path: compose '(help "whatever")/foo/bar

Where you could "accidentally wind up with a nameless void in a slot where it was allowed to have stringlike meaning".

But if most voids aren't nameless, then that would be kind of a rare concern.

Anyway, you could also just say "~"/foo/bar like you would for any other component that didn't LOAD as a WORD!, but it's nice to imagine such a very common case having some kind of exemption.

I've gotten an implementation working and it's looking great. As @BlackATTR has pointed out there's a lot of parts in the box and a lot of nuance, so anything that helps people get their bearings and reinforces the mechanics is important. This is exceeding the expectations I had for it in that regard.

(I'm actually kicking myself for not just trying this out sooner. It wasn't at all hard to write, and I really have thought about this a long time!)

But I hit a couple of things that might benefit from some design thought:

Right now the evaluator considers VOID! to be an error if it is seen literally. So this would be an error:

 >> do [10 + 20 ~void~ 3 + 4]
 ** Error: Can't evaluate VOID!s

I'll point out that this is a Ren-Cism that historical Rebol/Red don't care about:

rebol2>> do [10 + 20 #[unset!] 3 + 4]
== 7

r3-alpha>> do [10 + 20 #[unset!] 3 + 4]
== 7

red>> do [10 + 20 #[unset!] 3 + 4]
== 7

Yet it feels important for the sake of not letting stray unsets that get COMPOSEd into code be silently tolerated.

If VOID! can't be literally evaluated, and you're writing code in a literal context, then you'd have to write:

foo: '~whatever~
foo: lit ~whatever~

That's less satisfying than foo: ~whatever~. Although one of the allures of the tildes is looking "bad", this is not the kind of bad I'd have in mind.

One way of looking at this could be to say it's a feature of SET-WORD! / SET-PATH! assignment to look at the thing on the right, and if it's a literal void, accept it. Just make that narrow exception. Which seems all right...but then you get to:

There you have an evaluative argument, and no SET-WORD!. So you either have to say [return '~] or [return lit ~] (or [return x: ~])

I anticipate returning named voids and assigning voids to be common enough that this would be a loss to the prettiness of the idea.

So continuing the idea of finessing this: Maybe functions that are willing to take an evaluated VOID! argument also do the literal-void-acceptance trick?

But I'll point out that tricks like this are always more nasty than they sound...what happens when you have something that tries to left-quote the VOID! that comes after an assignment? foo: ~void~ left-quoter. One of Ren-C's strengths is not sweeping such what-ifs under the table, so it's good to know that a trick like this wouldn't be without its ramifications.

Should This Be Attacked More Generally?

The main thing I'm trying to watch out for here is letting voids that get composed into places that they have no effect and vanish. That is, cases like do compose [1 (print "Hi") 2]. However, I've suggested before that discarded literals may hint at problems and thus need to be errors. In terms of things that have wasted my time, blocks I meant to be code being silently discarded probably have caused way more insidious problems than accidentally-discarded voids have...

Plus, if you want to make a VOID! in the API, it would be nice if you could say:

 REBVAL *undef = rebValue("~undefined~");

Off course you can't do that for WORD!s, you have to quote them. But it seems like a loss to make you put a quote there.

The best thing is probably to lay hope in a general solution for catching stray voids along with stray blocks or anything else, and say that VOID! is an inert type. This means hoping that dereferencing variables holding voids, passing them to arguments of functions that don't take them, and trying to use them in truthy/falsey spots is where you get your error leverage. That means their usage at the source level can stay clean.

But it means I have to put on my thinking cap and review that discard-literals-raises-error proposal to see if there's anything there.

1 Like

It would be nice if that worked.

path: '~/foo/bar

And I think, I never used tilde in an identifier anywhere.