Why Doesn't `(third [1 2])` Trigger A Range Check Error?

Quick question for clarity. I assume that

compose [ 1 (1 / 0) 3 ]

would be a math error. Why is

compose [ 1 (third [ 1 2 ]) 3 ]

not some kind of a range check error?

Systemically, we consider NULL to be a "soft" form of failure. It serves a signaling role that's a little like what NONE! tried to do in historical Rebol, but since it's not an ANY-VALUE! more functions treat it as an error:

>> third [1 2]
== ~null~  ; isotope

>> append [a b c] third [1 2]
** Script Error: append requires value argument to not be null

>> compose [1 (third [1 2]) 3]
** Script Error: Cannot use NULL in COMPOSE slots

The theory is that places which require an ANY-VALUE! will error down the line, and that having a lot of constructs that make it easier to react to the "soft failure" is a better tradeoff.

One of those constructs is MAYBE, which converts nulls to void, allowing a seamless opt-out of things like APPEND or COMPOSE:

>> append [a b c] maybe third [1 2]
== [a b c]

>> compose [1 (maybe third [1 2]) 3]
== [1 3]

So I guess I'll just say that experience has borne out that soft failure is a more convenient for functions like THIRD than raising an error. If anyone finds a case where they don't think so, I'd be interested to see it.

Nowadays there's an option on the table for raising definitional errors, which could easily be turned into nulls with try third (...)

>> third [a b]
** Error: Cannot pick 3 of BLOCK! (or somesuch)

>> try third [a b]
== ~null~  ; isotope

(I've mentioned that this is different from the limited design of Rebol's historical errors, e.g. if you said attempt [third [a b]] in an R3-Alpha or Red that errored, it would give null back...but so would attempt [thirrd [a b]] because the lack of definitional-ism means it couldn't discern errors arising from a direct call from typos or other downstream errors.)

For cases where you would have been trusting a THIRD that returns NULL to trigger downstream errors, this gives better error locality. e.g. append [a b c] third [d e] would blame the THIRD, not the append.

And for cases where you might not be able to trust that NULL wouldn't be interpreted as an error downstream, it would be more robust. Also you'd give readers a clue at the callsite when you actually were intending that the operation might fail by the presence of the TRY.

This comes down down to the behavior of PICK (since FIRST is a specialization of PICK). I was just thinking about that with respect to objects:

>> obj: make object! [x: 10]

>> pick obj 'x
== 10

>> pick obj 'y
** Error: y is not a field of object

>> try pick obj 'y
== ~null~

If we raised a definitional error out of pick in this case, then you could try pick and get null.

When you think about PICK in general beyond just the block case, it does seem like more uniformly giving an error which can be "softened" via TRY would be a good idea. I'll give it a shot and see what the effects are.

So I rather quickly ran into the fact that PICK is shared code with what you get when you are using a tuple. So pick block 3 acts like block.3, and pick obj 'field acts like obj.field, etc.

But there's an important difference: obj.field can run a function. And if you write try obj.field the expectation is that your TRY is processing the result of that function...not errors arising from the field selection itself. :-/

A thought that crossed my mind was that perhaps /obj.field could be a notation for "field presence is optional" and give back null if it's not there, and /block.3 could do the same thing. I'd often thought that refinements were wasted as being inert, and since they exist in other places meaning "optionality" then something like this could be an application.

But new APPLY uses inert refinements to good effect, so I'm reluctant to pursue something like that.

Long Story Short: There's no real reason why (pick block 3) and (block.3) can't have different error modes, but (try block.3) can't defuse a definitional error for the pick without generating a disconnect from (try object.funcname).

For the moment, picking things that aren't there gives null on blocks, and errors on objects. It's a bit misshapen but some code would be more laborious to write if things like block.3 would error, forcing you to use (try pick block 3). And you really would have to put it in parentheses in many cases, because expressions like (block.3 = 'foo) would require ((try pick block 3) = 'foo)

Maybe it should be laborious? Anyway, food for thought.