Reputation: 12366
Since the main source of non-deterministic exceptions is IO and you can catch exception only inside IO monad, it seams reasonable not to throw exceptions from pure functions.
Indeed what could so "exceptional" happen in a pure function? Empty list or division by zero are not really exceptional and can be expected. So why not use only Maybe
, Either
or []
to represent such cases in pure code.
There is a number of pure functions like (!!)
, tail
, div
which do throw exceptions. What is the reason for making them unsafe?
Upvotes: 11
Views: 521
Reputation: 48611
Sometimes, something that is true about how a program behaves is not provable within its source language. Other times, it may be provable, but not efficiently so. Still other times, it may be provable, but proving it would require a tremendous amount of time and effort on the part of the programmer.
Data.Sequence
represents sequences as size-annotated finger trees. It maintains the invariant that the number of elements in any subtree equals the annotation stored in its root. The implementation of zipWith
for sequences splits the longer sequence to match the length of the shorter one, then uses an efficient, operationally lazy technique to zip them together.
This technique involves splitting the second sequence multiple times along the natural structure of the first sequence. When it reaches a leaf of the first sequence, it relies on the associated fragment of the second sequence having exactly one element. This is guaranteed to happen as long as the annotation invariant is maintained. If this invariant fails, zipWith
has no option but to throw an error.
To encode the annotation invariant in Haskell, you'd need to index the underlying pieces of finger tree with their lengths. You'd then need each operation to prove that it maintains the invariant. This sort of thing is possible, and languages like Coq, Agda, and Idris try to reduce the pain and inefficiency. But they still have pain, and sometimes massive inefficiency. Haskell isn't really properly set up for such work as yet, and may never be great for it (that's just not its main goal as a language). It would be extremely painful, and also extremely inefficient. Since efficiency was the reason for choosing this implementation in the first place, that's just not an option.
Upvotes: 4
Reputation: 531948
The unsafe functions are all examples of partial functions; they aren't defined for every value in their domain. Consider head :: [a] -> a
. Its domain is [a]
, but head
is not defined for []
: there is no value of type a
that would be correct to return. Something like safeHead :: [a] -> Maybe a
is a total function, because you can return a valid Maybe a
for any list; safeHead [] = Nothing
, and safeHead (x:xs) = Just x
.
Ideally, your program would consist only of total functions, but in practice that isn't always possible. (Perhaps there are too many undefined values to anticipate, or you can't know ahead of time which values cause problems.) The exception is an obvious indication that your program is not well defined. When you get an exception, it means you need to change your code to either
Under no circumstances should "3. Continue running with an undefined value in place of the return value of your function" be considered acceptable.
(Some conjecture to follow, but I believe it is mostly correct.) Historically, Haskell didn't have a good way of handling exceptions. It was probably easier to check if a list were empty before calling head :: [a] -> a
than to deal with a return value like Maybe a
. That became less of an issue once monads were introduced, which provided a generic framework for feeding the output of safeHead :: [a] -> Maybe a
to functions of type a -> b
. Given that it is easy to recognize that head []
is not defined, it is at least simple to provide a helpful, specific error message than to rely on the generic error message. Now that functions like safeHead
are easier to work with, functions like head
can be considered historical relics rather than a model to emulate.
Upvotes: 7
Reputation: 5477
Certain functions have preconditions associated to them (!!
requires a valid index, tail
requires non-empty list, div
requires non-zero divisor). A violation of a precondition should result in an exception, because you did not obey the contract.
The alternative is to not use preconditions, but to use a return value that indicates whether the call succeeded or not.
These are all core functions, so they need to be simple to use, which is a big point in favour of preconditions with exceptions. They are also pure, so there is never a surprise when they fail: you know exactly when that will happen, namely when you pass arguments that violate the preconditions. But, in the end, it comes down to a design choice, with points in favour and against both solutions.
Upvotes: 3