Skip to main content
added 309 characters in body
Source Link
Michael Homer
  • 13.1k
  • 3
  • 27
  • 88

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.

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.

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.

Source Link
Michael Homer
  • 13.1k
  • 3
  • 27
  • 88

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.

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).