Revisiting `<opt>` in the Func Spec Dialect

First, Let's Look Briefly At The History Of <opt>

It was many years ago when <opt> first came on the scene as a way to say that a function argument would allow "NULL" as a possibility.

The reasoning was that there was a certain "state" which can be synthesized that represents a non-value.

Note: Back then, the state was the result of excising reified UNSET! from the system--and it was called VOID--a term that means something rather different today. To make things clearer I'm going to sanitize history here a bit and pretend like it was always called NULL.

Meanness Level Alpha: You Can't Even ASK What Type a NULL Is

In the earliest days, not only did NULL "not have a type"...it was not truthy or falsey...it generated errors when you tried to access it through words. It went through several iterations of meanness.

This is basically where things started:

>> select [a 10 b 20] 'c
; null

>> type of select [a 10 b 20] 'c
** Error: NULL Values Do Not Have Types

But that was a pain.

Meanness Level Beta: You Can Ask, But The Answer Is Still A Mean NULL

I was steadfast that there wasn't going to be a NULL! "datatype". But since NULL was the "non-answer" given back for a "non-valued" state...what if the TYPE OF a NULL was just... NULL?

>> select [a 10 b 20] 'c
; null

>> type of select [a 10 b 20] 'c
; null

While it was pleasing at some level, it didn't change the fact that NULL was still mean, and not accepted a lot of places...much like an UNSET! had been in Rebol2:

rebol2>> switch print "Rebol2" [10 [print "Disliked UNSET!s"]]
Rebol2
** Script Error: switch is missing its value argument

So the same problem would hit you all around the system if you were trying to do something with that NULL answer you got for types:

>> switch type of select [a 10 b 20] 'c [
     integer! [...]
     block! [...]
]
** Error: SWITCH does not allow NULL as its VALUE argument

Meanness Level Gamma: NULL Becomes Friendly

Today we've gotten to a situation where NULL is generally pretty friendly and accepted a lot of places. WORD!s can look up to NULL without erroring, so NULL doesn't need to be a "function that returns NULL".

>> switch type of select [a 10 b 20] 'c [
     null [print "Everything works just fine!"]
     integer! [...]
     block! [...]
]
Everything works just fine!

The whole paradigm surrounding unsetness has gone through a great rethinking with BAD-WORD!s and isotopes that I'm still discovering the interesting nuances of, but it's light-years ahead of old methods.

So...Back To TYPESET! Representation

From the beginning I knew there was not going to be a NULL! "datatype". And using NULL itself in a type block would be a bad idea. It was too easy to wind up with NULL values on accident...they were used to represent unset values!

In those days, imagine if this had been the way to say "the typeset can include NULL":

foo: func [arg [null integer! block!]] [...]

All right, but what if you had just some typo and a variable was incidentally null?

foo: func [arg [intteger! block!]] [...]

Would you want that function to now accepts nulls and blocks? :frowning:

Even though NULL no longer corresponds to "typo" states, it still is a state that values can be in for all kinds of reasons. I think null = type of null is the right answer, but that doesn't mean that the way to indicate a function is willing to accept null as an argument is if any variable that happens to be NULL appears in the type spec.

And NULL couldn't be in TYPESET! because you would have no way to FIND it -- FIND won't look for "NULL" in anything...and if it ever returns NULL that means the thing wasn't found.

So Why Exactly Have I Been So Opposed to a NULL! Datatype?

I feel like I've always had a good reason. If a TYPE OF operation can make a reified thing spring into existence out of the absence of a thing, I feel like something is out of whack.

There's a clear advantage if you're going to be chaining things.

x: (type of select data item) else [
    fail "A reified NULL! datatype doesn't permit this kind of handling"
]

Though that's a more modern intuition about it; I wasn't thinking about that when <opt> was made.

I think one angle is that I felt like the optionalness was somehow a "big deal" and needed to jump off the page for the spec more. NULL! just blended in.

foo: func [arg [null! integer! block!]] [...]
bar: func [arg [integer! block! group! path! null! word!]] [...]

Modern Thought: <opt> => [<null> any-value!]

With the introduction of new rules surrounding RETURN, I've made it more painful to get the "default" return behavior of being able to RETURN whatever you want.

This made me think it might be nice to build on the PARSE meaning of <any> as a replacement for "SKIP" (e.g. match one ANY-VALUE!) to get an easy RETURN spec:

foo: func [return: <any> ...] [...]

Then, <opt> could be shorthand for [<null> any-value!]

bar: func [return: <opt> ...] [...]

I can also imagine <opt> being a UPARSE tag as a shorthand for opt <any>.

>> uparse? [x: (stuff)] [sw: set-word!, g: group!, x: <opt>]
== #[true]

>> x
; null

>> uparse? [x: (stuff) "extra"] [sw: set-word!, g: group!, x: <opt>]
== #[true]

>> x
== "extra"

In any case, <any> and <opt> would thus join the other "top-level" shorthands <none> and <void> for function typeset specs. Such a shorthand feels pressing given my new strict rule regarding returns, I don't like having to type [return: [<null> any-value!]] just to be able to use RETURN. I can put up with [return: <opt>] and [return: <any>]

Newer, Weirder Options with More Datatypes

There are actually more wacky and crazy ideas possible these days. :crazy_face:

We know that refinements like /ARG are optional, they may be NULL in which case that means they were not supplied. I was just suggesting they might be the better branch notation for allow pure NULL to escape a branch. We could also make the type blocks build on this notation:

foo: func [
    return: /[integer!]  ; alternative to today's [<opt> integer!]
    arg1 [block!]
    arg2 /[logic!]  ; alternative to today's [<opt> logic!]
][
    ...
]

But looking at it, I don't feel it communicates as well. The tags just seem more literate.

For some reason, <null> looks oddly heavy though:

foo: func [
    return: [<null> integer!]
    arg1 [block!]
    arg2 [<null> logic!]
][
    ...
]

I guess a lot of it is what you're used to, but null is such a "vertical" word, it's like it has two !! in it:

foo: func [
    return: [<opt> integer!]
    arg1 [block!]
    arg2 [<opt> logic!]
][
    ...
]

I'm probably just being silly due to what I'm used to. The <null> is clearer, and I kind of like my suggestion that <opt> be shorthand for [<null> any-value!].

I Know This Is Long, Just Thinking Out Loud

I don't want to change anything until TYPESET! and DATATYPE! have more understanding. But as it so happens, I think that the change to RETURN to make it the locus of typechecking on functions is a step forward in the type system conception.

Design happens one step at a time, folks.

1 Like

First I didn't like /[.. ] , but with the explanation, that it's optional like any refinement, it makes perfect sense to me.