How Should `TAKE []` React? Null? Error?

Here is the behavior of TAKE in Rebol2:

rebol2>> block: compose [1020 (none)]
== [1020 none]

rebol2>> take block
== 1020

rebol2>> take block
== none

 rebol2>> take block
 == none

 rebol2>> take block
 == none

One thing I was bothered about was the inability to notice a difference between taking from an empty block and taking from a block containing a NONE!. Ren-C can resolve that with NULL as a unique response to taking from a block with no elements in it.

But there's another concern which BrianH expressed, about a routine that you expected to have a side effect that did not. If you write a line simply like take block and it doesn't take anything away, should it be silent about that fact?

Early Ren-C introduced TAKE* as distinct from TAKE...

Experiments at being prescriptive worried that getting back a NULL might not be enough to warn you about a misunderstanding. So we wound up with this:

ren-c>> take []
** Error: TAKE-ing from an empty block, use TAKE*

ren-c>> take* []
; null

To make things simple, TAKE was just a chained function derived from the lower-level TAKE*. This seemed "safer"...at the time, and addressed BrianH's (pre-NULL) concern...as well as my general overarching desire to use things that are "more checked".

But this may be the wrong place to get paranoid about the user's intent, and we should just be happy that the NULL offers a unique result that means nothing was taken.

When I wrote about "Make Your Own Safety" I asked what the odds were that we could guess the top pet-peeve that every programmer would have.

How many times have you been bitten by TAKE on an empty block you didn't know was empty? When you did, how likely was it that you blamed yourself vs. the tool?

Not only that, but what if you could get the "hardened" versions of TAKE and TAKE* in the files where it was bothering you as easily as:

 take*: :lib/take
 take: (=> non null lib/take)

This seems the better angle, and avoids making it feel like there's any kind of bias toward the "tics" that bother @rgchris, which the * may represent. Give people the power to bend fast to address the specific concerns that are bothering them with the bent and personalized/customized code they are writing to solve the problem they have right now.

I don't think I have been bitten by that. For my personal preference, having TAKE return NULL from an empty block seems better than throwing an error-- the latter behavior implies that the block! cannot be-empty, which, I don't know, seems un-block-like. Using TAKE then makes a block! less block-like. I can think of all kinds of interesting variants of block! structures (constrained sizes, LIFO/FIFO) which could be convenient and safer for some TAKE scenarios.

I think that this is a good example of something that fits in well with TRY, under the new definitional-error-powered conception:

>> block: []

>> take block
** Error: TAKE-ing from an empty block, if intentional use TRY TAKE

>> try take block
; null
2 Likes

I proposed this idea a year ago and have used it as an example many times, in explaining things that could be done with definitional errors (e.g. changing PARSE to RAISE an error if it couldn't process the rules and get to the end of input)...

...but I hadn't gotten around to switching TAKE to actually use the mechanism.

Today I've done it, and I think it's a good choice. Having functions RAISE errors instead of returning NULL is a good way of throwing in a "speedbump" to catch situations that are likely to be mistakes...with the simple answer of adding in a TRY if you want to get past the speedbump (or use EXCEPT to catch the error).

This is really only possible due to definitional errors!!

If you tried to make TAKE cause an error in Rebol2/R3-Alpha/Red, you would have to trap it via the closest thing they have to Ren-C's TRY (called ATTEMPT) which would put the call in a block:

rebol2>> attempt [take []]
== #[none]

But this would be very dangerous, as it would trap errors that weren't being emitted specifically by the TAKE. For instance, a typo:

 rebol2>> block: [a b c]

 rebol2>> attempt [take blockk]  ; typo
 == #[none]

Or it would gloss over a variable being of the wrong type:

 rebol2>> data: 1020

 rebol2>> attempt [take data]  ; taking an integer!
 == #[none]

These are simple examples, but there's no limit to how dangerous this is.

But with definitional errors, Ren-C can safely react to errors that are specifically emitted by TAKE...

FAIL vs. RETURN RAISE: The New Age of Definitional Failures!

:sunglasses:

1 Like