Why No-Argument Refinements Are ~okay~ or ~null~

(Note: Lifted this writeup out of another thread that otherwise is useless.)

Note that Ren-C had undergone hard-fought battles to confront problems like:

rebol2>> foo: func [/ref arg] [print ["ref:" mold ref "arg:" mold arg]]

rebol2>> foo
ref: none arg: none

rebol2>> foo/ref <thing>
ref: true arg: <thing>

rebol2>> foo/ref none
ref: true arg: none

rebol2>> condition: false
rebol2>> foo/ref if condition [<thing>]
ref: true arg: none

The above pattern made it hard for function authors to know if a refinement was in use. If they tried to go by the arg, they could be ignoring an inconsistent meaning where the caller literally meant to specify a none. The situation gets worse with multiple refinement arguments.

(Think about a case where you allow arbitrary values to be interleaved into a block with an /INTERLEAVE refinement, but no values will be inserted if the refinement is not intended. NONE! is one of the legal values, since it can be anything. If the function author heeds the refinement by name, then looks at the argument to see if there's a NONE!, it will work. But that prevents them from using the argument's status as none or not. The situation is confusing and you'd find functions making ad-hoc policy decisions, which may-or-may-not allow you to pass none as a way of backing out of a refinement when you used the refinement on the callsite.)

Gradually, the evaluator was made to keep the refinements' truth or falsehood in sync with the state of the arguments. Use of a NULL for all of a refinement's arguments at the callsite would make the refinement appear unused to the function, as if the caller had never specified it. Using NULL for only some of them would raise an error. And refinement arguments were never allowed to be themselves NULL... they were only nulled when the refinement was unused, and hence trying to access them would be an error.

This ultimately streamlined even further into the unification of the refinement and the argument itself...reducing the number of random words you'd have to come up with, shrinking call frames, eliminating a "gearshift" in the evaluator that opened doors to AUGMENT-ing frames with new normal arguments after a refinement argument.

But something to point out is that because these changes were incremental over time, ideas like the necessity of erroring on null accesses were also something that had to be challenged over time. I had a bit of uneasiness about things like:

rebol2>> foreach [x y] [1 2 3] [
             print ["x is" x "and y is" y]
         ]

x is 1 and y is 2
x is 3 and y is none

Something about running off of the edge of the data and not raising so much as a peep was unsetting. Doubly so because a NONE! might have actually been literally in the array. It seemed that once you had the power of NULL to distinguish, that not taking advantage of that with error checking would be a waste...

But such checks have upsides and downsides. Look at R3-Alpha:

r3-alpha>> data: [#[none!] #[unset!] 1020]
== [none unset! 1020]

r3-alpha>> data/1                          
== none

r3-alpha>> data/2  ; Note: prints nothing in console (and no newline)
r3-alpha>> data/3
== 1020

r3-alpha>> data/4
== none

Here's Ren-C:

>> data: [_ ~ 1020]
== [_ ~ 1020]

>> data.1
== _

>> data.2
== ~

>> data.3
== 1020

>> data.4
; null

Is this so bad, not to error on data.4 or other null cases? At least a distinct state is being signaled...that you can tell is out of band of what's in a block.

I think that when all angles are examined about how things have progressed, this makes sense, as does the unused-refinements-are-null change. It may feel a little more unsafe, but if we're using JavaScript...

js>> var data = [null]  // can't put undefined in array literal...
js>> data.push(undefined)  // ...but you can push it (?)
js>> data.push(1020)

js>> data
(3) [null, undefined, 1020]

js>> data[0]
null

js>> data[1]
undefined

js>> data[2]
1020

>> data[3]  // out of bounds (fourth element, indices are 0-based)
undefined

I think Ren-C is leading the way here, and that it's not worth getting bent out of shape over NULL accesses not triggering errors. The TRY mechanisms are there to pick up the slack when there are questionable uses of a NULL, to make sure it was intended.

1 Like