~null~ WORD! antiform vs. BLANK! antiform ~_~ as NULL

The concept of having isotopic ~true~ and ~false~ as WORD! antiforms instead of having a distinct LOGIC! type is admittedly somewhat weird. It meant they couldn'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 antiforms

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

  • Your circumstance may mean that neither of those choices are what you want when in a situation where the antiforms 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 an antiform--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.

NULL Was Once ~_~ Antiform, But Now ~null~ antiform

Once upon a time I called antiform BLANK! null. It was not pretty looking:

>> null
== ~_~  ; anti

>> _
== ~_~  ; anti

It was because I made BLANK! evaluate to its antiform. This meant if you assign a bunch of variables to null, it gave a pretty visibility to the actual values to pay attention to:

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

I was reticent about using the word antiform ~null~, because 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~ ; anti), it seemed 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 was 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 antiform blank, we'd get renders like:

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

Which I actually liked that less than seeing ~null~ there.

I was torn, but Chose The WORD! Antiform ~null~

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 "antiform blank" instead of null. And trying to teach people "antiform 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
 ]

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.

My general intuition was that from a user's standpoint, it's an uphill battle to teach them antiform 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 (_ => ~_~ ; anti) evaluation. Recovering blank for space intent and having them be unevaluated was good

Some Technical Difficulties of ~null~

It had some of the same problems as having to deal with the type checking of LOGIC! actually being an antiform subclass of two words. It means "NULL!" is an isotopic subclass of one word, so there's no specific null datatype.

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! antiform 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.

1 Like

Just got snowed on. :snowflake:

So I'll stay inside and recap the implications of making ~null~ a WORD! antiform:

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.


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 "trash"
      • 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 antiform
    • a block antiform 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 antiform itself
    • making NULL an antiform 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! antiforms, will decay to their first result. So you'll never find a block antiform in a variable.

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

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

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

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

>> reify null
== null

>> meta null
== ~null~

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

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

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

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

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

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

3 Likes

A post was merged into an existing topic: Why shouldn't VOID be done with a ~void~ WORD! Antiform?