~null~ WORD! isotope vs. BLANK! isotope ~_~ meaning null

The concept of having isotopic ~true~ and ~false~ as WORD! isotopes instead of having a distinct LOGIC! type is admittedly somewhat weird. It means they can't be put in blocks and need some sort of transformation if they're going to be put into blocks.

  • The generic and reversible way to make them real it is to META them and get ~true~ and ~false~ quasiforms, which you can UNMETA back to the isotopes

  • The readable way to make them real is to REIFY them and get true and false as plain WORD!, which only gets turned back into the isotope form if they are bound to variables that hold the isotopes, and you evaluate them.

  • Your circumstance may mean that neither of those choices are what you want when in a situation where the isotopes can't work...maybe it means you have to rethink what you're doing, or maybe you want some other transformation. It's good to be alerted to the fact that it's dodgy to put what you think of as a "logic" in a reified context.

One should note that Lisp has no false at all, only NIL for false (since everything else is truthy, you could use anything else for true, but they have T predefined). But their NIL isn't isotopic--so it can appear in lists literally.

And as I've repeatedly pointed out, Redbol's historical choice to render LOGIC! conflated with the words true and false (instead of #[true] and #[false]) shows a desire to avoid the logic literals "escaping" into the reified consciousness of the user. Making the logic forms actually impossible to put into blocks without a conscious transformation feels like it's good.

I know it's a strange choice, but it's seeming like it fits with the territory. It's a sort of tradeoff you need when you choose to be firm that TRUE and FALSE are redefinable words and not lexical forms of logic constants.

So... Why Shouldn't NULL Be Done With ~null~ isotope ?

Right now what I call "null" is isotopic BLANK!. It is not pretty looking:

>> null
== ~_~  ; isotope

>> _
== ~_~  ; isotope

But what is pretty looking is that if you assign a bunch of variables to null, the fact that blanks evaluate to the null isotope gives a great visibility to where the actual values to pay attention to are:

obj: make object! [
    alpha: _
    beta: _
    delta: true
    epsilon: ~
    gamma: "nutty"
    rho: _
    omega: 'now
 ]

If we used a word isotope, then at source level we're writing:

obj: make object! [
    alpha: null
    beta: null
    delta: true
    epsilon: ~
    gamma: "nutty"
    rho: null
    omega: 'now
 ]

And if we view it after evaluation that fetches (null => ~null~ ; isotope), it gets uglier:

make object! [
    alpha: ~null~
    beta: ~null~
    delta: ~true~
    epsilon: ~
    gamma: "nutty"
    rho: ~null~
    omega: 'now
 ]

We can't show it as the WORD! null because there's no guarantee that word always will evaluate back to the null isotope. But the concept is that since BLANK! can't be redefined, we can put it in an evaluative context as a substitute for ~_~ if we want.

If we were "more honest" and just META'd the isotopic blank, we'd get renders like:

make object! [
    alpha: ~_~
    beta: ~_~
    delta: ~true~
    epsilon: ~
    gamma: "nutty"
    rho: ~_~
    omega: 'now
 ]

But I might actually like that less than seeing ~null~ there.

Things To Weigh In This Consideration

I'm really torn. Certainly having people be able to see ~null~ written out as a word corresponds to what we want to refer to the state as. We aren't going to replace people's vernacular to say "isotopic blank" instead of null. And trying to teach people "isotopic blank is null" is an uphill battle.

Let's look back at that seemingly beautiful situation with all the blanks-to-nulls at source level:

obj: make object! [
    alpha: _
    beta: _
    delta: true
    epsilon: ~
    gamma: "nutty"
    rho: _
    omega: 'now
 ]

But, hmmm. The ~ is now an assignable state meaning variable is not set. How often will you want to set something to NULL...which won't generate an error on access like being unset would, but is falsey and can't be passed to many routines without a MAYBE?

Statistically, we might often have a situation where NULL is the minority initialization, more like this:

obj: make object! [
    alpha: ~
    beta: ~
    delta: true
    epsilon: null
    gamma: "nutty"
    rho: ~
    omega: 'now
 ]

My point is that intentionally initializing things to null may not be as common as setting to an error-provoking unset state. And when null happens it may be just as noteworthy to call out as setting something to true or false.

Are There Technical Barriers To This?

It has some of the same problems as having to deal with the type checking of LOGIC! actually being an isotopic subclass of two words. It means <opt> is an isotopic subclass of one word, so there's no specific null datatype. :-/

I was already saying that TYPE OF NULL was probably an error, and TYPE OF MAYBE NULL would be void... anyway, this fits into some of the same type issues that logic has.

Previously NULL cells held nothing, so I'd put a payload of the file and line that were in effect in the evaluator. This was supposed to give better errors about where a null originated from. This could still be done if NULL was a special case of WORD! isotope that said it was null via a flag vs. storing the symbol, but could be complicated. None of the better error mechanics were done yet.

My general intuition is that from a user's standpoint, it's an uphill battle to teach them isotopic blank is something called "null"... and that the majority of technical problems that are involved in making NULL be a word isotope are problems that have to be tackled anyway with true and false as word isotopes.

Additionally, I've written about my desire for BLANK!s to serve in dialects as spaces. That gets very screwed by the (_ => ~_~ ; isotope) evaluation. Recovering blank for space intent would be good.

I'm going to likely give it a shot, when I get some programming time again, hopefully soon.

1 Like

I might as well throw in the adjunct question of: "Why shouldn't VOID be done with ~void~ isotope?"

It's worth noting that regardless of whether NULL is represented as ~_~ isotopes or ~null~ isotopes, it used to merely be said as having "no representation". That's true of isotopes also, but there's been a standard that the interface shows an isotope by printing the quasiform and then commenting it as "oh, it's actually an isotope state"

At that time, the console had a special exception for displaying nulls. And the "quoted" form of null was just a single apostrophe with nothing after it:

>> null
; null

>> quote null
== '

>> '
; null

This was all conceived before the idea of having a meta state for void, because voids just vanished. But as things shuffled around, I came to think that this "no representation" made more sense for voids...

>> '
; void

>> quote void
== '

>> meta void
== '

>> 1 + 2 '
== 3

Having a META state that is quoted vs. quasi puts VOID in a bizarre middle ground of not being isotopic. Yet void can't be put in a block. Though I've argued that we might also see it as that you can put infinitely many voids in a block...

>> append [a b c] '
== [a b c]

 >> append [a b c] comment "hi"
 == [a b c]

 >> first []
 == ~null~  ; isotope (new proposal as word isotope vs isotopic blank)

 >> append [a b c] first []
 ** Error: Can't append ~null~ isotope (see MAYBE)

 >> maybe first []
 ; void

 >> append [a b c] maybe first []
 == [a b c]

Generally speaking I'm pleased with that. Though it means if you wind up setting a variable to void, it will look kind of slight:

 >> make object! [x: comment "hi"]  ; might require a SET/ANY or similar
 == make object! [
     x: '
 ]

Why not push it the other way, and say that void states are isotopic?

One key value of saying that void states are "ordinary" is that it means they have an isotopic form, and that isotopic form is rendered as just ~.

This isotopic void state of ~ is being used as the "truly unset" state. An even meaner idea of complete absence of value than void. I'm of the belief that it's important to have this not be the same thing as void, because the tolerance of void by so many constructs (including things like ANY and ALL to skip them, or opting out of APPENDs, or making SELECTs just return a null).

The design of the system is such that the quote byte of 0 means isotopic status (e.g. "quote level negative 1" is represented by 0), and the void type is a 0 byte. So you can memset() an area to 0 and get all unset states (typically an optimized operation). And the operating system clears all memory to 0 before giving it to your application as well. It's kind of pleasing that such a representation gives you a nice unset state.

It seems to me that the pieces of the puzzle fit together better this way.

3 Likes

Just got snowed on. :snowflake: So there's an incentive to stay inside and program...

Hence I've gone ahead and done this ~null~ change, and am looking over the implications.


My first impressions are that it feels right, though I definitely will admit that there's a lot of subtlety to it. To try and put the whole thing into perspective.

  • The first attempt by Ren-C to make a state that could not be put in a block called it "void", and it was the only non-valued state.

    • It was used as the contents of an uninitialized variable
    • It was the result of a function like HELP that didn't want to show a result
    • It was the result of a PICK out of a block which was out of range
    • It was the result of a SELECT out of a map when the key wasn't present

    This "non-valued" state took on a number of Redbol NONE!'s responsibilities, but was "ornery" in the way Redbol UNSET! was--hence it could not be tested for truth or falsehood.

  • Realizing that the orneryness of not being testable by logic was inconvenient, the concept was changed to be called "null" and be falsey

    • The API exposed NULL results as C/Javascript null pointer
      • this was conveniently also falsey in those languages
    • The term "void" was reclaimed to describe invisibles, like COMMENT
    • Unset variables held another ornery state called "none"
      • calling the state "unset" seemed semantically wrong
      • variables are unset, not the contents of a variable

    This was a great development for the API, and working with blocks was still rigorous. But for some other questions, being able to tell the difference between "no answer" and "there's an answer, but it's null" still presented a puzzle.

  • Isotopes were introduced as a nuance on a value that would only be detectable by those who cared.

    • "heavy null" and "light null" were the first isotopic concept
      • heavy null meant "there is an answer, but it's null"
      • ELSE would only react to light null (no answer, not even null)
      • heavy null would decay to light null on variable assignment

    The isotope mechanic became generalized (as explained here):

    A Justification of Generalized Isotopes

  • With notable features pertaining to splicing intent (group isotopes) and errors (error isotopes), a new way of saying "there is no answer" vs. "there is an answer, but it's null" came out of using block isotopes as multi-return signals.

    • null itself no longer needed an isotopic form
    • a block isotope with a null represented in it could be "heavy null"
      • this required null to have a meta-representation to be in a block
      • but META of NULL had been defined as NULL
      • null was thus rethought as being an isotope itself
    • making NULL an isotope of the WORD! null seemed the cleanest answer

An important point to make is that there's now only one form of isotopic decay: multiple return results, e.g. BLOCK! isotopes, will decay to their first result. So you'll never find a block isotope in a variable.

It's pretty wild to look at how it all works.

>> first [~null~]
== ~null~

>> ~null~
== ~null~  ; isotope

>> null
== ~null~  ; isotope (null WORD! is defined to be ~null~ isotope)

>> reify null
== null

>> meta null
== ~null~

>> third [a b]
== ~null~  ; isotope

>> third [a b] else ['c]
== c

>> if true [null]
; first in pack of length 1
== ~null~  ; isotope

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

>> if true [null] else [123]
; first in pack of length 1
== ~null~  ; isotope    

I know it's a lot to take in, but I feel like everything is there for a reason...

3 Likes

You don't always know how you're going to feel until you've tried something a while. But I have to say I'm starting to like ~null~ a lot more than I expected.

In fact, I was just reading some code:

let meta: meta-of action
let notes: null
let description: null

And I thought to myself... hey. That doesn't feel like it's "calling out" the nulls as much as I might like (!) I almost felt like changing it:

let meta: meta-of action
let notes: ~null~
let description: ~null~

Add oddly enough, that's faster...because it doesn't need to fetch the definition of the WORD! null.

I'm not actually going to change it. But the fact that I thought about it makes me comfortable with the decision to go this route. It feels more natural than I expected.

1 Like