Historical Redbol only had WORD!s in the BLOCK! that came after a parameter:
foo: func [
arg [block! word! number!] ; all words in block
] ...
But when Ren-C introduced the paradigm-breaking NULL that could not be put in arrays, that meant there was no null!
datatype. To fill the gap, the tag <opt>
was chosen to indicate the parameter as optional--hence possibly null:
foo: func [
arg [<opt> block! word! number!] ; now it's WORD!s and TAG!s
] ...
I liked using a TAG! for how it stood out (though in retrospect I'd have probably chosen <null>
, but everything was named differently then). Other quirky ideas were floated, like being able to put a leading slash on the typeset block:
foo: func [
arg /[block! word! number!] ; like a refinement, but on the types
] ...
That didn't gain traction, and probably shouldn't have.
Then when early efforts faced another value state that couldn't be put in a block and didn't have a type, <void>
came onto the scene...because as with there being no NULL!, there was no VOID! datatype.
Tag Modifiers Which Weren't Type Checkers Showed Up
The ability to take a parameter but get an immutable view of it was added as <const>
.
Parameters that would accept being at the end of input and evaluate to null in that case were <end>
<variadic>
and <skippable>
came into existence.
These "parameter-control tags" seemed to me to be a distinct category from typecheckers like <opt>
and <void>
. Having them all use TAG! felt like too many tags.
So I mused about splitting the roles, something like:
[<const> #null type!]
-or-
[#const <null> type!]
But I didn't like the look of it enough to move on it. So things like [<const> <opt> type!]
stuck around while I wondered about it.
Today, You Can Specify Any Type Check By Function
There's still no NULL! or VOID!. But with the way things work now, you can use functions as "type predicates" to recognize things that aren't datatypes in their own right:
foo: func [
arg [null? block! word! number!]
] ...
What's good:
- It leaves TAG! for the properties like
<const>
that don't have to do with type recognition... but rather controlling the parameter in a more special way.
What's bad:
- It loses that kind of special look that tags gave to arguments that could take null. It blurs together, especially with things like SPLICE? and LOGIC? and CHAR? for other non-fundamental datatypes (characters are just single-character issues now, and ~true~ and ~false~ isotopes of WORD! implement logic)
A Modern Option: ~NULL~ for Taking Null
I made an experiment so if you used a QUASI-WORD!, then it would match an isotope of that form.
It's a kind of pleasingly distinct look:
foo: func [
arg [~null~ block! word! number!]
] ...
And it mixes better with the tags:
foo: func [
arg [~null~ <const> block! word! number!]
] ...
It also works for "nothing" (isotopic blank) and doesn't look too bad there, e.g. for RETURN:
foo: func [
return: [~] ; as opposed to `return: [nothing?]`
] ...
Allowing NULL? and ~NULL~ As Choices Seems Good
I like the option of ~null~ instead of null? to call out the more rare-and-alarming idea of accepting null parameters.
What about return: <nihil>
and return: <nothing>
These two special uses of tag! with no block have been used to say you don't need a RETURN statement at all... the function just gives back none or nihil respectively when the body completes.
How necessary is it? Well, you either write things like:
comment: func [
return: [nihil?]
discarded [any-value!]
][
return nihil
]
Or you have the contraction:
comment: func [
return: <nihil>
discarded [any-value!]
][
]
This style of "don't even worry about writing a RETURN" has the widest applicability to NOTHING and NIHIL. We don't strictly need it, but I've gotten used to it.
I mention return: [~] as a possible alternative for saying nothing is a return type using the quasiform-means-isotope idea. And since nothing falls out of function bodies by default with no return, it's not strictly necessary to have return: <nothing>
as any kind of special operation.
Again, how does that look?
foo: func [
return: [~]
] ...
A little more symbol-y, but doesn't break the rhythm of type specs being blocks.
This leaves the nihil case. We could say return: [~[]~] and have that mean "I return an empty pack" but in that case you'd still need an explicit return:
comment: func [
return: [~[]~]
discarded [any-value!]
][
return nihil
]
But I think I like return: [nihil?]
better than that. Compared to return: [~]
the [~[]~]
feels a bit like a bridge too far. But I might change my mind.
Anyway, the reason this is a struggle is that return: <nothing>
has just become so pervasive that it's hard to see that changed to return: [nothing?]
. But standardizing on blocks and moving away from the tags for this application may be the best idea.