"type of antiform" now possible... Should We Use It?

The way antiforms were implemented originally didn't allow for the idea of giving a coherent answer back from TYPE OF. So they all gave the same answer, much how QUOTED! and QUASIFORM! all gave the same answer:

>> type of first [''a]
== |~{quoted!}~|  ; antiform

>> type of first [~a~]
== |~{quasiform!}~|  ; antiform

>> type of spread [a b c]
== |~{antiform!}~|  ; antiform

So there was nothing like ~{splice!}~. The only way to type check antiforms was with constraints, like SPLICE?... which was slower, since it had to call a constraint function.

So I reworked things in a way that speeds up SPLICE? to the point where it's no more expensive than checking for INTEGER! (or INTEGER?).

As a curious byproduct that I didn't even initially intend, this changed the behavior and it started treating antiforms as being datatypes:

>> type of spread [a b c]
== |~{splice!}~|  ; antiform

I was actually surprised, because I didn't intend to do it. It's not often a feature "just happens".

But Is It A Good Idea For Antiforms To Answer TYPE OF?

I've struggled with types, and the question of what TYPE means, vs. what KIND means... or CLASS?

If we develop some sort of class system, then TYPE becomes a loaded question. If you have an object that somehow remembers it was created as a BOOK!, then do you get book! = type of my-book being true? Or is TYPE OF forever committed to returning the fundamental type and saying it's an OBJECT! ?

This hesitance has affected my willingness to rename REB_BLOCK and REB_BLANK to be TYPE_BLOCK and TYPE_BLANK, which would be much more palatable (and survive a language name change away from Rebol...)

There's also the question of type of null, which I feel needs to be an error most of the time. The way I've worked it out so far is that you get a FAILURE!. So if you know you want to test for null you can say try type of null and get a null result--not a datatype, but there's a sort of reasonable logic to saying the type of null is null.

What Would It Hurt, e.g. if SPLICE! Looked Like A Type?

For instance, is there a really good reason why this shouldn't work?

>> to splice! [a b c]
== |~[a b c]~|  ; antiform (splice!)

>> to block! ~[a b c]~
== [a b c]

switch type of spread [a b c] [
    splice! [print "Is this somehow bad?"]
]

It might be confusing, that a type ending in ! can't be put into a block. But that was true before with antiform!...

I lean toward thinking that there's more upside than downside. And I kind of want to put this TYPE vs KIND vs. whatever else issue to rest. We write type of x and expect to get something like BLOCK! or OBJECT! back. If there's something more complex than that, it probably needs to be done another way with class of or base of or similar.

So if you have something like ''''1 and ''''x and ''''3, and you want to know which are the "same type", you'll need to define your operators for answering that.

Want to know if they're the same underlying type when you take the quotes off?

 (heart of x) = (heart of y)

Want to know if they're at the same quote level?

(quotes of x) = (quotes of y)

I'm sort of tempted to say actually that it might should be that TYPE OF doesn't work on quoted values, because of how misleading it is... but all blocks are of the same type, even if they're different lengths. And it is actually often the case in dialects that if something is quoted, you treat all quoted things the same way...oddly enough.

In any case, the new flexibility for -OF operators means we can get some other ideas in the mix. Random brainstorms:

>> quoted-type of first of ['''x]
== |~{' ' ' word!}~|  ; antiform (datatype!)

>> quoted-type of first of ['''1]
== |~{' ' ' integer!}~|  ; antiform (datatype!)

If we say there are more questions you can ask of values to produce structure... instead of putting that burden on TYPE, we can maybe commit to what type does and move along.

And maybe there could be something like class:relax of x which if something wasn't an object, would give you the fundamental type?

>> class of some-novel
== &[object! ...]

>> class of some-block
** Error: not an object, use CLASS:RELAX to permit other types

>> class:relax of some-block
== |~{block!}~|  ; antiform (datatype!)

I guess in other words, I'm feeling like it's time to just go ahead and say that TYPE-OF is the question we build on, that it returns fundamentals or antiform classes.

And maybe the concept I've always wanted of calling it TYPE! instead of DATATYPE! will be possible, too.

1 Like

Having run with this for a year now, it seems to be a good decision. The groundwork is laid so that TYPE OF can correctly answer for "extension types" as well, which provides unlimited new fundamental datatypes coming from extension DLLs. (There are some questions about name collisions--like what happens if two extensions use the same name for their new types--but that's not the most burning issue right now.)


As things developed, I decided to narrow the WORD! antiforms to just be ~null~ and ~okay~, and let this be the LOGIC! antiform class. It just came to seem that leaving the door open to other antiforms wasn't as valuable as constraining it.

Historically I'd had reservations about type-related questions like INTEGER? accepting nulls, thinking that NULL should be more "disruptive". If integer? my-null-var came back false I thought that seemed to be too tolerant. So I made you say integer? cond my-null-var (COND(ITIONAL) turns NULL into a "veto" which forces a NULL answer of whatever function you give it to, without running the function).

I think my prescriptivisim got a bit out of hand here, and saying type of null is LOGIC! makes sense for today's world.

But notably, if you aren't really testing for LOGIC! and just want a null-in-null-out answer for TYPE OF, you can still use type of cond.

switch type of cond x [
    null [...]
    integer! [...]
    block! [...]
    panic "Expected null, integer, or block"  ; ~okay~ would trigger this
]

I think that's probably usually more likely to be what you want to test for than LOGIC!.

Further pushing back on the prescriptivism, I think that erroring on NULL for tests like INTEGER? haven't turned out to be all they're cracked up to be. Being an antiform stops NULL from spreading into some places, but it's a stable antiform... so it can be conditionally tested, and things like SPLICE! can too. So why shouldn't (integer? null) just be null?

Saying that INTEGER? fails on undecayable unstable forms such as VOID! or TRASH! is likely the better rule.

What About TYPE OF Unstable Antiforms?

It seems "obvious" that type of should decay its argument. So if there's an unstable antiform test it would be something like type* of.

Though a bit weird today is that ACTION! is an unstable antiform an evolution of piggy-backing instability through ACTION!-in-PACK!, and it decays to FRAME!. That's a bit of an awkward tension, as old tests broke:

>> action! = type of append/
== |~null~|  ; antiform (logic!)

>> frame! = type of append/
== |~okay~|  ; antiform (logic!)

What feels frustrating about this is that you also get this behavior:

>> action? append/
== |~okay~|  ; antiform (logic!)

>> frame? append/
== |~okay~|  ; antiform (logic!)

That's because ACTION? is based on taking a ^META parameter, while FRAME? is not.

Overall I think it's wise to think of ACTION! as unstable, because things like FOR-EACH aren't willing to put unstable antiforms into non-^META variables. This helps when you're doing enumerations and not prepared for things that might be ACTION!s... you know you did for-each ^x [...] so you can connect that with using ^x in the loop body.

Yet there's a contradiction that x: func [...] [...] is assigning an unstable antiform without you having to say ^x: func [...] [...]. That's an ergonomic compromise...it's true also for x: ~ assigning voids or x: ~<foo>~ for assigning trash. Hence it's only PACK!s that decay in non-meta SET-XXX cases.

Right now that implies the rules for set $x: ... vs. set $x ... are different; the latter has protection against unstable antiforms (to get that it would need to not decay packs, you'd have to decay them yourself).

Assignment exemptions aside, ACTION! decaying to FRAME! feels like a safe compromise that's less uncomfortable than it feels. The ergonomics are good; because words being "active" is a "special" thing that you kind of want to quarantine.

TYPE OF In Any Sophisticiated Language is Tricky

Consider for instance that arrays in C and C++ can act like pointers. But if you're asking "is this a pointer" whether something says yes or not depends on whether you "decay" it. A non-decayed array says no, a decayed one says yes.

(There's bizarre mechanics like whether you say decltype(x) vs decltype((x)) which get you at the "raw information" vs the "when used as an expression".)

It would be possible to say TYPE OF gave errors on unstable antiforms instead of implicitly decaying them. So maybe type of pack [1 2] panics, but type of decay pack [1 2] would be INTEGER!. But that feels wrong.

Types are a mire, and I still feel like the right answer to doing more pattern-based type-checking hasn't shown itself.