Should Things Like TEXT? Fail On NULL

One of the principal ideas of the null state is to be a falsey state a variable can hold that reflects it hasn't been assigned... but that you can access without getting an error on word fetch.

Increasingly I've come to believe that functions should not accept NULL states as arguments, unless they are specifically designed for doing conditional tests (e.g. IF needs to take NULL in its condition slot...)

I think that if you write code that says if not text? var [...] and VAR was null, that should be an error to cue you into the fact that your test was meaningless. We now have empty splice that can serve a role of a "friendlier" form of nothing (called a "HOLE"). So you can use that for your variable if you need tests like this to succeed.

If you really want to "opt out" and get the test to fail on null, there's OPT which will pass a void... and then that will let you say "I meant to do that".

Should Testing Quoted Values Soft Fail?

I kind of feel like you're potentially missing some of the picture if you're working with quoted values and don't realize it:

>> foo: first ['<thing>]
== '<thing>

>> tag? foo
== ~null~  ; anti

I mean, it is a tag... it's just a quoted one. Same with pinned/lifted/tied values.

>> bar: @<thing>
== @<thing>

>> tag? bar
== ~null~  ; anti

These could give ERROR! back, and you could say try tag? bar if you wanted to say "yeah I know it's quoted, and I want that to not be a match".

(I've discussed leading colon as a shorthand for TRY in the modern world, so it could be :tag? bar in that world, for people who like brevity.)

I don't think having a speedbump here is as likely to be as inconvenient as one might think. And it could cue you to ask tag? noquote bar or tag? plain bar or whatever your particular meaning was.

I'm not as confident about this as I am about the need to panic (not error) on null.

What To Call The Typeset?

I think the set of things that we're talking about the signature testing for here is "every stable value state except for KEYWORD! and TRASH!"

I've struggled with the SOMETHING? term and the over-generality of NOTHING?... and here I think we don't want ~okay~ to pass either ... and ~okay~ seems like something.

TESTABLE? :face_with_diagonal_mouth: TYPECHECKABLE?

Well, I'll have to think about it. In any case, I'm sure enough that I want to start failing on NULL.

I talk about how the things you map from in MAP! need to support antiforms:

If it helps in naming this "not a keyword! and not a trash!" class, that constraint is the same as would be applied here in what's not legal to use as a key.

So in at least some sense, the class is LEGAL-KEY?

In terms of devil's advocacy and wondering how far to take this...

Should things like EQUAL? prohibit NULL, too?

You can test for nullness with NULL? and NOT. So is there any great reason why you should be able to say if foo = null ?

Remember, if you have a variable that happens to be null that you want to test against, you can test with opt and compare the voids.

something: null

; maybe you assign SOMETHING, maybe you don't...

if #A = something [...]  ; illegal

if #A = opt something [...]  ; legal

It's Probably More Trouble Than It's Worth, But...

...it does seem like forcibly making you do an OPT on a compare has advantages that come in general from other places that require it.

I'll keep an eye out for arguments for or against it...

When I look at something like try tag? bar the only thing that jumps out to me of why the question would fail would be if BAR was a function that generated an ERROR!, and you tried to test the error's type.

I wrote about that here:

Typecheck and ERROR! ... pass through vs. :RELAX ?

You kind of only get one axis of "thing to definitional error about"; it shouldn't mean too many things. And that's the meaning I'd likely lean toward.


Where this runs into trouble is not in the equality test itself, but in things that use equality in their implementation. Let's say SWITCH, for example.

var: null

switch opt var [
    ^void [print "You'd do your equality matching on voids"]
]

You might be able to get away with writing that as:

switch opt var [
    () [print "Voids might match Voids for EQUAL? purposes"]
]

Maybe that would work but it feels unclear if you thought you were testing against nulls. You could reify it:

switch reify var [
    '~null~ [print "You'd do your equality matching on voids"]
]

I guess it just pushes too far away from being able to write:

switch var [
    none [print "You could do this in historical Rebol"]
]

That says what it means and means what it says. So I don't think we can really drift too far away from that without making things unclear. The safety isn't worth it.

But I Do, So Far, Like Not Accepting NULL? in the type checks.

In my experience this just doesn't tend to mean what you think it means:

 value: null

 if not tag? value [...]

It's usually a bug, and I don't mind needing to write if not tag? opt value [...] -- it has caught real problems.

But it would be nice if there were some way to set this as a policy, e.g. on a per module (or per-function?) basis? Just don't know quite how to do it, as RebindableSyntax has some tools for this but they break down past one layer of calling a library function.

"Oh what a tangled web we weave..."

So this has caught bugs. But it also introduces new scary ones.

If I'm getting you to write if not text? cond var [...] or if not text? opt var [...] and say that PACK! or NONE are failing... we get to the issue of pack? cond var or splice? opt var

Type testing kind of has to be honest. Because if you transform a value from null into another value and type test it, you're going to hit the point in the matrix where the thing you transformed to was the thing you were testing for.

And now that I'm thinking that NULL is LOGIC!, it really is starting to seem like making your variables NULL as the "easy to test for falsey state" is a mistake... or at least, a stylization offering some risks and tradeoffs... not unlike setting a variable to NONE.

I now believe the right thing to do if you are concerned about this is to make the variable access fail before it gets to the type test.

This suggests using GHOST! more frequently as the "variable not set yet state", and then leaning more into the "if you want it as NULL, use :var" notation.

That is forcing my hand in several areas:

  • ghost needs to error, and you use ^ghost to get the ghost variable literally.

  • Unused refinements have to be GHOST!, and if you want to test them for null you use :refine

    • This finesses an old question about how to deal with unused refinements having a common state (e.g. why 0-arg refinements in Rebol2 are NONE and not false)... use :refine and the ghost becomes a null
  • text? null is simply NULL

  • Passing a FAILURE! to a type test should propagate the failure, so you try text? fail "hi" can be NULL... this doesn't apply to the FAILURE? test, of course.

Switching NULL and OKAY to be of type LOGIC! is Big

A lot of implications are coming from narrowing the WORD! antiforms to just two...for all time.

It's a bit strange how when you push the ripples around that comes to a conclusion like "therefore, refinement arguments need to be accessed with :refine and not just refine. But that's how these things go.

1 Like