8
$\begingroup$

Many languages prioritize runtime safety by forcing users to handle (or annotate) the error cases for operations that can fail.

This works well for I/O and complex algorithms, but how should common fallible operations, like division and array access, be handled? Every a / b can technically fail if b is zero, and every list[index] can fail if index is out of range.

However, those operations are far too widespread to surround each instance with cumbersome error handling, or annotating with "can throw division by zero".

How can a general-purpose programming language have this sort of operation without creating too much boilerplate for users, and without giving up safety with runtime panics?

Bonus points if the solution is usable for integer overflow, since it includes almost all arithmetic operations.

$\endgroup$
6
  • 1
    $\begingroup$ Too minor to be an answer, but these operations could return an Option or Result, and your language could have some sugar for "unwrapping" those (e.g. a safe navigation operator such as ?., or ? like in Rust). $\endgroup$
    – user
    Jul 1, 2023 at 21:10
  • 1
    $\begingroup$ Even rust doesn't let you use ? for division by zero or array indexing, they just panic $\endgroup$
    – mousetail
    Jul 1, 2023 at 21:14
  • $\begingroup$ @user I think that's a perfectly ok answer, especially because Rust itself opted to not have division and array access return Results. If you find out why, that's an A+ answer. $\endgroup$
    – BoppreH
    Jul 1, 2023 at 21:14
  • 2
    $\begingroup$ I’ll note that Swift, the language that was fine with adding jo ud2 after every add and imul, said that returning an optional on array access would make it unacceptably slow. $\endgroup$
    – Bbrk24
    Jul 1, 2023 at 21:26
  • 1
    $\begingroup$ If your language only has floats and no separate integer types, then you wouldn't need to worry about a / 0 at all thanks to +/-Infinity and NaN. $\endgroup$
    – user
    Jul 1, 2023 at 21:47

1 Answer 1

4
$\begingroup$

Every a / b can technically fail if b is zero, and every list[index] can fail if index is out of range.

a / b can be rejected statically by a type system that ensures that the divisor is non-zero. In many cases this will be obvious, and in others it will require validation (which should be there anyway). Flow-sensitive typing will tend to make this more convenient, and types like "non-zero integer" can be propagated through parameter types so that they can be determined at the earliest possible point.

It is possible to handle list indices similarly, ensuring that the index is within the domain of the list. This will be still more complex, but also already necessary to ensure there isn't an error. A good dependent type checker will ensure that the validation is present when required, and not require adding more where the property has already been established. Again, parameter types can ensure this, relating a parameter used as an index to the list it corresponds to.

Suitable numeric range types can also prevent arithmetic overflow, although they will need to correspondingly limit what you can do: multiplication of two values needs upper bounds whose product is within the size of the container. The annotation burden of these is likely to be a bit higher than the other cases because of this.

It's likely that most realisations of this will reject many programs that would operate correctly in practice. This is a trade-off against correctness and verbosity.


Another approach taken in some languages is simply to reject the idea that these are errors in the first place: JavaScript is perfectly happy with division by zero, it just results in Infinity, and an out-of-range index produces the undefined value or silently extends the array. These signal values can lead to undetected problems later on — notably, Infinity and NaN values are numbers, so even type tests don't help — and mostly move validation logic to later on at best.

In Perl, "autovivification" is an explicit language feature at a deep level: $h{"a"}[2]{"c"} = 1; automatically constructs the layers of intermediate hashes and arrays when non-existent indices are used. Raku inherits this approach too.

Similarly, cyclic treatment of list indices can make sense in some cases — we had a question recently about just that. In this case, there's no failure for any index into a non-empty list, though it may be a logic error.


Calculations that can fail in the middle due to dynamic values are exactly what the option or maybe monad are for. For strings of computations where a mid-stream failure means the whole thing fails, these allow writing the code directly and having an offramp when needed.

A number of Haskell libraries provide division or subtraction operations of the form (Num a) => a -> a -> Maybe a: binary operations that take two numbers, and return an option type, and these work with the monadic do notation for easy handling:

result = do
   a <- getValue
   b <- getOtherValue 2
   x <- div a b
   y <- normalise x
   return (y * 100)

At the end, result is either Just (normalise(a / b) * 100) or Nothing, and can be typecased to handle it at the end. If normalise also errored out, the result is still Nothing. There's no need for localised error handling for the specific division, and no non-local propagation of exceptions.

One realisation of this approach outside of heavily-monadic languages is sometimes called "railway-oriented programming": there's a happy track, where things are working and everything just runs in sequence, and an error track where any problem gets redirected to. This is very like the monadic do above, but generally allows more handling code to run alongside starting at different points in the calculation. Sometimes it's permitted to repair and return to the successful pathway. You can find existing libraries for this in F#, Ruby, Kotlin, and other languages.

Option types without the monadic treatment are also possible, although they will generally be more cumbersome for the programmer to use. They could be worthwhile if this level of correctness is desirable, or if the language provides easy unwrapping or chaining for options (like the ? and ?. suffixes in some modern languages).

$\endgroup$
0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .