Should TRAP and CATCH return null if no fails/throws?


#1

R3-Alpha’s TRY added a refinement /EXCEPT for passing in a block or function to act as a handler in the case of an error being raised. It struck me as a clunky attempt to parrot existing terminology. TRAP was created to be a seemingly better name than “TRY”. It appeared to have more parity with CATCH, and TRAP/WITH paralleled CATCH/WITH.

I think the name is an improvement. And it paved the way for the short word TRY to fill an important role in converting nulls to blanks.

But there’s a pattern in both of these constructs which is that they return a result whether something is caught or trapped or not.

>> catch [10 + 20]
== 30

>> error? trap [make error! "this is *not* a trapped error"]
== #[true]

That particular behavior of trap is particularly tricky because many cases check if a result is an ERROR? and use that as a detection of if a FAIL ran…and here we see that’s not actually happening.

So if you’re trying to write truly “correct” code that uses a TRAP or a CATCH, you pretty much wind up writing a TRAP/WITH or a CATCH/WITH…providing a function taking an error.

But I’ve been questioning the value of this mixing-up-of-return-results. If you want to get a value out of the block, why not do that by setting a variable? You’re usually trying to set a variable anyway, e.g. value: trap [...], what’s wrong with moving it into the code?

 trap [
     value: some-calculation-that-may-fail ...
 ] then func [e] [
     ... code to handle the error ...
 ] else [
     ... stuff to do if there was no error ...
     ... assume value is good ...
 ]

This cleanly separates out the code paths, allowing usage of null-sensitive constructs. So it means getting rid of the /WITH refinement on TRAP and CATCH, instead using normal THEN/ELSE/etc. constructs with them:

>> catch [10 + 20]
// null

>> trap [make error! "not a thrown error"]
// null

In the case of CATCH, you can always just throw your final result, to get it to conflate with an ordinary throw (this is inexpensive.)

TRAP can’t do that (since it would only be able to return errors). Though you could piggy-back on CATCH if you really wanted to avoid variable declaration with a TRAP…just throw your result:

 catch [trap [... throw result] then e => [e]]

I’m not opposed to the idea of code-golf-friendly constructs which could go ahead and do this squashing of results together. (CATCH-DO, TRAP-DO?) But the clean expression with only returning the caught or trapped thing–and null otherwise–seems quite appealing to me for the primitive building block.

Also note: the existence of ENTRAP

I made ENTRAP to address the problem of distinguishing errors from other values, by returning ordinary values enclosed in a block (unless it’s null, which can’t be put in blocks, so the entrapped thing is just null)

>> entrap [null]
// null

>> entrap [10 + 20]
== [30] // note it's in a block!

>> error? entrap [1 / 0]
== #[true] // it's just a plain ERROR! value, not in a block

>> block? entrap [make error! "abc"]
== #[true] // it's a block that contains an error (since it wasn't FAIL'd)

I don’t know how that fits into the naming and scheme of things, but mentioning it.


New datatype idea: SINK!
#2

One issue with this is that CATCH and THROW have a “/NAME” feature, where you can label throws. Rebol2/R3-Alpha/Red all have the feature of being able to name a throw, but you cannot tell what the name is when you catch things…you lose that information.

By having CATCH take a handler function, Ren-C made that handler able to take two arguments…the value passed to the throw and the name. You didn’t have to pay attention to the name, but you could give a list of names to CATCH and then check to see if any of the names you were looking for were present.

So how would this be collapsed into a single value? It could be done by saying that CATCH/NAME would implicitly assume you understand you’ll be getting a BLOCK! with the name in the first cell, and the value in the second.

>> catch/name [
       throw/name 10 'alpha
   ] [alpha beta]
== [alpha 10]

(I specifically went with the value in the second slot so that it could be missing–e.g. a length 1 block–in order to convey a THROW of a NULL. I think being able to throw nulls is likely important; while it does cause some confusion regarding whether the CATCH actually caught something or not, it is probably worth it to have the ambiguity for the flexibility.)

That’s not the worst thing in the world. But it is a bit clunky. The other option is that you pass the name in as a variable…which is kind of how other things have been done (historically, DO/NEXT, for example…when it wanted to return a new position as well as the evaluated value)

>> catch/name [
       throw/name 10 'alpha
   ] 'name
== 10

>> name
== alpha

But that doesn’t seem to fit very well with the THEN construct:

 catch/name [...] 'name then value => [...]

So being able to wrap together the name and the value into one unit seems cleaner.


#3

I am not sure I like this. Being able to use then/else is great, but I don’t particularly like having to move assignments into the code.

Var: trap […]

Makes it clear, that this is an assignment, which, by the way, may error so I’m handling this.

trap [var: …]

looks like it is mostly error handling code, and the assignment is easily overlooked.


#4

Compare:

 if error? data: trap [	              
     inflate/max data uncompressed-size	           
 ][
     info "^- -> failed [deflate]^/"
     throw blank
 ]

With:

 trap [	              
     data: inflate/max data uncompressed-size	           
 ] then [
     info "^- -> failed [deflate]^/"
     throw blank
 ]

What makes the assignment clearer? I think the second case, in particular because it doesn’t conflate data as a variable which “may hold an error, or may hold the data”. If you wanted to be “clear” you’d have to call it data-or-error, which is too wordy.

It seems to me the second way puts the data: closer to what’s being assigned to it, instead of separating it in an awkward and artificial way. The first case doesn’t make it that obvious that inflate/max returns a value at all–maybe only TRAP does? The only hint you have that you’re not just capturing an error is the misleading name “data”. If you mix up the data with the error that means you’re going to need a test, and if error? data: trap is a lot of noise to see through.

(And that’s a simple case that doesn’t mention the specific error when it probably should. You could use ATTEMPT … ELSE for this, which would just give you a null if it was an error otherwise the value. But I worry about today’s ATTEMPT because it can make typographical errors or other changes hard to find, so it seems it should be improved to at least not scuttle some common errors of words not being bound.)

More complicated examples which are trapping a section of code that doesn’t just do a single assignment are even better.

What makes me feel better about it as a primitive is that it prevents mistakes in generic code like:

if error? item: trap [
    someone-elses-array: get-array-may-fail x y z
    pick someone-elses-array index
][
     // may be an ERROR! value that just was in someone-elses-array
]

Again–I don’t object to there existing some construct that conflates raised errors with plain error values, and has all the concerns which go with that. But if this behavior is built in to trap, then I start worrying and thinking that it should turn non-raised errors into voids, or otherwise help avoid these kinds of mistakes. It seems the best way is to give a solid routine to build on, then let people do what they like with that.

Being able to get rid of TRAP/WITH is clean, and you can also use use ELSE to provide clauses easily for the non-erroring case. I think it’s an improvement.

What about RESCUE for the combined operation?

Perhaps the conflating operation–which would have no /WITH refinement–could be called RESCUE? Terminology-wise that seems a bit more vague. TRAP and CATCH feel like being NULL makes sense if there was no fail or throw. (e.g. if trap [...] [... an error was trapped, so you definitely had an error! ...]) But rescue doesn’t have that baggage.

I still would probably want to turn errors returned from the body normally into voids. If you have an error and really want to return it, you’d use a FAIL (in a parallel to what I suggested for using THROW as the last line of a CATCH if you want to return the value)

 value-or-error: rescue [
     someone-elses-array: get-array-may-fail x y z
     item: pick someone-elses-array index
     if error? :item [fail item]
     :item
 ]

But doing that filtering requires getting things into a variable anyway, so you should have just used TRAP.

So if looking at historical Rebol2-style code, you generally could turn plain TRY into RESCUE, though I’d still suggest using TRAP instead and putting your assignments inside the block.


#5

It seems, that my comment wasn’t completely thought through.
You have convinced me.