I've been experimenting with the idea and I'm sharing it so that you can tell me what you think.
Basically, let's say I have a block with some of the operations possibly throwing an overflow - or to be more precise one of the compiler intrinsics (e.g. __builtin_sadd_overflow return true).
So, initially, I had something like this:
try:
# do something
# do something else
if operation_with_builtin_sadd_overflow: raise newException(ValueError, "")
# do more stuff
if another_operation_with_builtin_sadd_overflow: raise newException(ValueError, "")
except CatchableError:
# handle this case
But then, I thought that's pretty much an overkill and decided to wrap the whole thing (in try) in a named block with each of the "risky" operation wrapped in a template that breaks out of the parent block in case the operation returns true (that is: an overflow).
So, the code above has become like:
template overflowGuard(main: untyped, alternative: untyped): untyped {.dirty.} =
block overflowBlock:
main
return
alternative
template tryOp(op: untyped): untyped =
if unlikely(op): break overflowBlock
proc add*(a: mySth, b: mySth): mySth =
overflowGuard:
# do something
# do something else
tryOp: operation_with_builtin_sadd_overflow
# do more stuff
tryOp: another_operation_with_builtin_sadd_overflow
do:
# handle this case
I'm not sure what the real gain is but it sure looks simpler and lighter. (regarding produced code too).
Are there any legitimate reasons why something like this wouldn't be preferrable?
Awesome! Thanks for the feedback!
I've been rewriting Arturo's internal Rational numbers module (basically, I want it to support both "normal", 64-bit int/based Rational numbers and GMP BigNum-based Rational numbers, pretty much like all numbers in Arturo), so I got totally caught up in this (and tons of overflow checks as usual).
If anyone is curious to have a look, here's the code: https://github.com/arturo-lang/arturo/blob/cleanup-vrational-implementation/src/vm/values/custom/vrational.nim
I've used this style quite a bit. There are some pros and cons though. For these simple test-based things it's not really an issue, but one thing to keep in mind is that the test based approach basically requires the caller to directly deal with any errors or lose them forever. Exceptions on the other hand can be "accumulated" in that the direct caller might ignore them, but the caller of the caller can deal with all the possible exceptions. So the usecase is really a bit different in that regard. And of course since they can be accumulated you have the whole stack-trace ability and error message passing system which adds a bit of weight on to the exception handling system.
So in general I'd say they are two quite different approaches to some very similar-but-not-exactly-the-same issues.
When it comes to trivial arithmetics, you're often better off not using exceptions at all (they're ugly, heavy) - instead you can simply perform all side-effect-free operations and check later on if they failed, ie:
var failed = op1()
failed = failed or op2()
The key idea here is that you collect the error and only check once at the end, without the use of exceptions and without, after each individual operation, checking if it overflowed.
This is good for all kinds of reasons: performance, brevity etc and relies on the observation that adding numbers has no long-term side effects - if something failed along the way, which presumably happens rarely, you don't want to pepper your execution pipeline with branching, memory allocations and other heavy code - this completely negates the point of using these builtins to begin with.
The idea is not new - floating point operations work exactly this way with nan simply propagating through the computation allowing you to check it only once, at the end.
Now, assuming you really want to check every operations separately, you can us Opt instead, from nim-result:
proc f: Opt[int] =
? operation_with_bultin_sadd_overflow(...)
? another_operation_with_builtin_sadd_overflow(...)
let x = f().valueOr:
# handle error
The key idea here is that you collect the error and only check once at the end...
I've contemplated the idea although I didn't try it in practice. Definitely makes sense... I'll try it! Thanks for the suggestion! :)