Dialect Meaning of Non-Words in Function Parameter Spec Blocks

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 "trash" (isotopic voids) and doesn't look too bad there, e.g. for RETURN:

foo: func [
    return: [~]  ; as opposed to `return: [trash?]`
 ] ...

That doesn't work for voids, and the current idea is that there's no VOID! because TYPE OF VOID gives back NULL. This is consistent with void-in-null out in general, although it might confuse people who expect to do:

switch type of value [
    null [print "This means value was void, not null"]
    ...
]

So the idea there was that perhaps TYPE OF NULL is an error, and this guides you to another solution like switch/type where you can use &[null?] or &[void?] as type predicates and get what you actually want.

(I still don't think defining null!: &[null?] is a good idea, because it looks like a fundamental datatype, and you cannot make null! ... etc. Maybe null?!: &[null?] or something like that would be a bit less noisy at usage sites, enough to be worth it?)

Allowing NULL? and ~NULL~ As Choices Seems Good

I think we can live with void? in function specs for functions that deliberately take voids. It's also more legitimate--voids are more neutral than nulls in the current formulation--e.g. append [a b c] void is [a b c].

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: <trash>

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 NONE and NIHIL. We don't strictly need it, but I've gotten used to it.

I mention return: [~] as a possible alternative for saying trash is a return type using the quasiform-means-isotope idea. And since trash falls out of function bodies by default with no return, it's not strictly necessary to have return: <trash> as any kind of special operation.

Again, how does that look?

foo: func [
    return: [~]
 ] ...

If you're going to break the pattern and not say return: [trash?] then I may call that a break-even alternative to return: [trash?]. 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 [~[]~] is a bridge too far.

Anyway, the reason this is a struggle is that return: <trash> has just become so pervasive that it's hard to see that changed to return: [trash?]. But standardizing on blocks and moving away from the tags for this application may be the best idea.