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

I wanted to package up the parameter typechecking functionality into a TYPECHECK function that you could use:

>> typecheck [integer!] 1020
== \~okay~\  ; antiform

>> typecheck [integer!] <foo>
== \~null~\  ; antiform

I decided that it should probably pre-decay values by default... just like a normal parameter would. So a pack with two integers in it would look like an integer!, not a pack:

>> typecheck [integer!] pack [10 20]
== \~okay~\  ; antiform

>> typecheck [pack!] pack [10 20]
== \~null~\  ; antiform

This corresponds to the behavior of a non-^META argument when used as a parameter. It can't receive packs, so if your typecheck incidentally allows packs that's irrelevant. Instead you have to use a ^META parameter convention:

>> typecheck:meta [pack!] pack [10 20]
== \~okay~\  ; antiform

But ^META parameters are able to decay to "coerce" to the parameter requirement, so integer! would still work if you used the :META refinement:

>> typecheck:meta [integer!] pack [10 20]
== \~okay~\  ; antiform

Okay, But What About ERROR! Values?

It is relatively quite rare that you intend a type checking operation to pass through an unstable ERROR! antiform as if it was intentional.

So you certainly need to specify a :META check. But that shouldn't really be enough:

e.g. this seems like a dangerous answer to me:

>> typecheck:meta [integer! pack?] 1 / 0
== \~null~\  ; antiform

It would be one thing if ERROR! was specifically in the typeset, but if it's not, that's likely going to allow accidents to pass through.

There could be a :RELAX refinement. But I had a different idea... what if it just passed the ERROR! through, as the result of the check?

>> typecheck:meta [integer! pack?] 1 / 0
** Error: Divide by Zero

>> (typecheck:meta [integer! pack?] 1 / 0) except e -> [e.id = 'zero-divide]
== \~okay~\  ; antiform

>> try typecheck:meta [integer! pack?] 1 / 0
== \~null~\  ; antiform

That's strictly more powerful than a :RELAX refinement, and looks a little nicer and briefer.

This Got Me Thinking, What If More Type Checks Did This?

Today, many type checking functions pre-decay their inputs, so errors are promoted to panics:

>> integer? 1 / 0
** PANIC: Divide by Zero

But some of the ^META-oriented functions are not as finicky; they take their arguments undecayed, and then don't bother to report ERROR! as a problem:

>> pack? 1 / 0
== ~null~  ; anti

You can imagine people wanting it either way...maybe they want to gloss over ERROR!, and maybe they don't.

The same pattern could apply here. Pass through the ERROR!. People can say try pack? if they want errors to count as not passing the check, otherwise if there's no triage with EXCEPT or another routine then it will be promoted to a panic.

I like this idea for the boolean tests (ERROR? being an exception, that returns null/okay).

But type of is a bit different.

I'd already "used up" the ERROR! return state by saying type of null is an ERROR!, such that try type of null gives you null.

It's not technically impossible for a function to return ERROR! for different semantic reasons, but it does make it harder to draw meaning from try being NULL if you've got multiple ways to get it.

TYPE OF doesn't take its parameter as ^META, so it pre-decays.

>> type of pack [1 2]
== \~{integer!}~\  ; anti (datatype!)

Given that fact, we could say that it uses VOID-in-NULL-out:

>> type of ^void
== \~null~\  ; anti

>> type of opt 1 = 2
== \~null~\  ; anti

And we could say that ~okay~ is its own type:

>> type of 1 = 1
== \~okay~\  ; anti

Then, ERROR! could be propagated and tested with TRY. So if you're thinking you want to handle ERROR! you could do that, and it would be distinct from the null case.

I'm not totally sure about this, and in part because type of has started to become a lot less useful than it was in the past.

Knowing that a: is a CHAIN! isn't all that informative, because :b and a:b are too. So destructuring is a lot more relevant than knowing fundamental types tends to be.

But I do feel that glossing over ERROR! and NULL is misguided.

>> type of null
** I think this should set off an alarm by default

>> type of fail "boom"
** I think this should also set off an alarm by default

Being able to smoothly handle the null case strikes me as important, but that's now done elsewhere with OPT:

>> integer? null
** Error: NULL antiforms have limited typechecking (e.g. KEYWORD?)

>> integer? opt null
== ~null~  ; anti

TYPE OF should probably align with that. As for whether ERROR! gets propagated, I don't know if that's important or not...