What Should TYPE OF an Isotope Be?

At this exact moment...TYPE OF any isotope is an error, while both TYPE OF NULL and TYPE OF VOID give back NULL.

OF is a generic operation (the enfix form of REFLECT that quotes the word on its left. It may be desirable to honor the VOID-in-NULL out convention for all the other reflectors that aren't TYPE... and it's desirable to error on NULL more generically.

>> label of null
** Error: You didn't give anything

>> label of maybe null  ; MAYBE NULL is VOID 
; null

So if type of follows the same pattern as other xxx of, we'd surmise that you don't use TYPE OF to discern NULL and VOID. It errors on NULL input and gives you back NULL if you MAYBE it.

But what happens when you ask:

>> spread [d e]
== ~(d e)~  ; isotope

>> type of spread [d e]

The Original Plan Was No Arguments Received Isotopes

In the original conception, function frames weren't capable of holding isotopes in the arguments. You physically could not receive a parameter that was an isotope.

I was also looking at the idea that some isotope forms--such as isotopic ERROR!--would be completely impossible to get into a variable, ever.

The only workaround was if a function used the ^META parameter convention, in which case an isotope would come in as a QUASI! form of the value...while normal values would come in as quoted:

 >> detector: func [^x] [print ["Meta of X:" mold x]]

 >> detector [d e]
 Meta of X: '[d e]

 >> detector spread [d e]
 Meta of X: ~(d e)~

Ultimately I backed down on this, instead allowing you to use type filtering to narrow which isotopes you'd be willing to accept:

>> splicetaker: func [x [any-value! ~group!~]] [
       append [a b c] :x

>> splicetaker [d e]
== [a b c [d e]]

>> splicetaker spread [d e]
== [a b c d e]

So this creates a new typeset feature that is hacked in terribly (the presence of any ~xxx!~ currently means the same thing as ~any-value!~, but it's documentation for when the feature gets better). And it means that the ^META parameter convention is basically something that can be implemented on top of this, by doing something like x: my meta at the beginning of the function.

A primary driver behind this change was that operations which wanted to do things like ADAPT a function frame were having to become sensitive to whether a parameter was ^META or not, and it was more of a headache than having to use the :get-word accessors on variables. It seemed that standardizing the frame in a way that permitted isotopes as currency made more sense than having arguments be sometimes-meta'd, sometimes not.

What if OF (REFLECT) Didn't Take Isotopes?

So we could say that if you think you have an isotope in your hand, you're responsible for ^META-ing it yourself:

>> metatyper: func [x [any-value! ~group!~]] [
       print ["Metatype of X is" type of ^x]

>> metatyper [d e]
== @['block]  ; the TYPE OF received a QUOTED!, so e.g. answer incorporates quoted

>> metatyper spread [d e]
== @[~block~]  ; got QUASI!, so TYPE OF answer incorporates quasi

On the plus side of such an approach, we don't have to invent any type representations for isotopes.

Devil's advocacy here :japanese_goblin: would say that if parameter types need to be able to take isotopes, then the type mechanics have to be able to represent the "isotopic types" somehow.

And if there's a representation, there would be uses in usermode for it. But it would have to be a special operator, and what it returned probably shouldn't be "a type" in the conventional sense.

For instance: It would be disruptive to do something like suddenly say all the types are actually "one level quoted up"...just to get this representation at quote level 0. This would give isotopes the &[~xxx~] representation, but at the cost of making everything else skew with an extra quote:


>> type* of first [(1020)]
== &['group]

>> type* of first [''(1020)]
== &['''group]

>> type* of first [~(1020)~]
== &['~group~]

>> type* of spread [d e]
== &[~group~]

That's pretty awful and misleading, just to be able to get that last representation...which you generally wouldn't use for anything beyond comparison anyway.

What if the result were itself a QUASI! of the type?


>> type* of first [(1020)]
== &[group]

>> type* of first [''(1020)]
== &[''group]

>> type* of first [~(1020)~]
== &[~group~]

>> type* of spread [d e]
== ~&[~group~]~

That's some fairly outside-the-box thinking (pun intended), which keeps the other types looking appropriate for their input. It has the nice touch that while all the real datatypes are unevaluative, the quasiform would evaluate into something that is sort of nasty, to show that you're probably confused. So while it isn't an isotope (and you wouldn't want it to be) it has that going for it, still allowing its raison d'etre of comparison:

splice!*: '~&[~group~]~
activation!*: '~&[~action~]~

switch type* of :x [
   splice!* [...]
   integer! [...]
   activation!* [...]

There I'm spitballing the idea that the isotopic "types" are made to stand out by having an asterisk after them.

(Bear in mind that this technique should ideally only come up in fairly advanced code.)

These quasi forms wouldn't actually be a datatype...

This would mean typical things designed to react to TYPE-BLOCK! wouldn't react to it.

But is that a bad thing? Consider trying to use it in PARSE:

>> splice!*: '~&[~group~]~

>> parse [? ? ?] [some splice!*]
; ** What were you expecting to do with it if it WAS a datatype? **

You can't match it against anything you'd find in a block, only evaluative products. No harm done in PARSE by not being a "real type".

On the other hand, you might imagine using it with something like MATCH:

 >> match splice!* spread [d e]
 == ~(d e)~  ; isotope

So match could be written to say it takes these forms (let's say we call them "isoTYPES")

datatype!: &[type-block]

isotype!: &[~type-block~]

match: native [
    test [datatype! isotype! block!]
    value [any-value! any-isotope!*]

Should it go this far? I don't know.

Pragmatically, I Think We Need The TYPE* Operator

I'm reluctant to see [any-value! any-isotope!*] typesets popping up everywhere. Part of the whole religion of isotopes is that they only show up in arbitrary outputs, and anything that takes isotopes in will only take very specific ones...as an alternative to trying to slipstream that state into a refinement or something.

But...I've struggled with some convolutions you get when you try to write code without being able to branch on types including isotopic ones. And as I mentioned, typesets kind of need it too in order to work internally.



Above I suggest an XXX OF consistency, with TYPE OF NULL as an error and TYPE OF VOID is NULL.

If that were so, and TYPE* OF VOID is not NULL, then it would be the only case where TYPE* OF would change an answer from what the TYPE OF would say.

Does that suggest not allowing plain TYPE OF either NULL or VOID, and requiring you to use TYPE*, to get some kind of reified answer? (This would introduce null!* and void!*)

Or does it suggest allowing NULL and not VOID? (This would introduce null! with no *, and void!*)

>> type of null
== &[null]

>> type* of null
== &[null]

>> type of void
** Error: Use TYPE* to take type of isotopes if intentional

>> type* of void
== ~&[null]~

But I've Always Been Uncomfortable With NULL!

When discussing why I made the <opt> tag instead of null!, I said:

I Dislike The Idea Of TYPE* Changing Answers

Imagine you had code like this:

 switch type of (...) [
    null [...]
    integer! [...]

But one day you realize the input could be a splice or something, and so TYPE* gets swapped in:

 switch type* of (...) [
    null [...]
    integer! [...]
    splice!* [...]

It seems to me that the meaning of NULL should not change, all things being equal. But things are probably not equal.


This Is Very Aggravating... but...

I think I like TYPE OF NULL being NULL

  • I like it even if it breaks a pattern in XXX OF where VOID input means NULL out, and NULL input is an error

  • TYPE feels like a fairly special kind of question, so it's probably okay

Internally we need a way to put nulls and voids into typesets, and I can imagine usermode cases where this is useful as well.

  • If TYPE* is able to give a reified answer of NULL!* and VOID!* this would provide some uses

  • This would imply that TYPE* OF NULL and TYPE OF NULL would be different answers

It might be best to avoid a symbol in the type, to signify the weirdness of it. But in the current design, NULL actually isn't an isotope...void is the isotope of NULL:

>> type* of null
== &[]

>> type* of void
== ~&[]~

I suspect I'm missing something here. But as with most things, I guess I just have to kind of try things and feel them out.

So what I'm going to try is a world where function parameters are powered by this TYPE* concept, give the same concept to usermode, and see what emerges from that.