Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the purpose of throwing exceptions in pure functions?

Tags:

haskell

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?

like image 990
Konstantin Milyutin Avatar asked Apr 13 '16 11:04

Konstantin Milyutin


People also ask

What is the purpose of throwing an exception?

The object, called an exception object, contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception.

Is throwing an exception a side effect?

It should be clear that throwing an exception violates referential transparency, and consequently has side-effects.

Does throwing an exception return the function?

No, because throwing an exception is not a return. If the exception is not handled inside the function it will cause an immediate exit out of the function, passing control to the first point in the program where the exception will be catched ie handled.


2 Answers

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

  1. Avoid calling the function on the value for which it is not defined
  2. Replace your function with a function that is defined on the problem value.

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.

like image 176
chepner Avatar answered Oct 29 '22 23:10

chepner


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.

An example

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.

like image 30
dfeuer Avatar answered Oct 29 '22 23:10

dfeuer