24

I've been told that in functional programming one is not supposed to throw and/or observe exceptions. Instead an erroneous calculation should be evaluated as a bottom value. In Python (or other languages that do not fully encourage functional programming) one can return None(or another alternative treated as the bottom value, though None doesn't strictly comply with the definition) whenever something goes wrong to "remain pure", but to do so one has to observe an error in the first place, i.e.

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

Does this violate purity? And if so, does it mean, that it is impossible to handle errors purely in Python?

Update

In his comment Eric Lippert reminded me of another way to treat exceptions in FP. Though I've never seen that done in Python in practice, I played with it back when I studied FP a year ago. Here any optional-decorated function returnsOptional values, which can be empty, for normal outputs as well as for a specified list of exceptions (unspecified exceptions still can terminate the execution). Carry creates a delayed evaluation, where each step (delayed function call) either gets a nonempty Optional output from the previous step and simply passes it on, or otherwise evaluates itself passing a new Optional. In the end the final value is either normal or Empty. Here the try/except block is hidden behind a decorator, so the specified exceptions can be regarded as part of the return type signature.

class Empty:
    def __repr__(self):
        return "Empty"


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        raise TypeError("Optional has no boolean value")


def optional(*exception_types):
    def build_wrapper(func):
        def wrapper(*args, **kwargs):
            try:
                return Optional(func(*args, **kwargs))
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper


class Carry:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
    1
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
    1
    >>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __rshift__(self, step) -> "Carry":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value
  • 1
    Your question has already been answered, so just a comment: do you understand why having your function throw an exception is frowned upon in functional programming? It's not an arbitrary whim :) – Andres F. Oct 27 '16 at 18:42
  • 6
    There is another alternative to returning a value indicating the error. Remember, exception handling is control flow and we have functional mechanisms for reifying control flows. You could emulate exception handling in functional languages by writing your method to take two functions: the success continuation and the error continuation. The last thing your function does is call either the success continuation, passing the "result", or the error continuation, passing the "exception". The down side is that you have to write your program in this inside-out fashion. – Eric Lippert Oct 27 '16 at 19:59
  • 3
    No, what would be the state in this case? There are multiple problems, but here are a few: 1- there is one possible flow that isn't encoded in the type, i.e. you cannot know whether a function will throw an exception just by looking at its type (unless you only have checked exceptions, of course, but I don't know any language that only has them). You are effectively working "outside" the type system, 2- functional programmers strive to write "total" functions whenever possible, i.e. functions that return a value for every input (barring non-termination). Exceptions work against this. – Andres F. Oct 27 '16 at 20:51
  • 3
    3- when you work with total functions, you can compose them with other functions and use them in higher order functions without worrying about error results "not encoded in the type", i.e. exceptions. – Andres F. Oct 27 '16 at 20:53
  • 1
    @EliKorvigo Setting a variable introduces state, by definition, full stop. The entire purpose of variables is that they hold state. That has nothing to do with exceptions. Observing a function's return value and setting a variable's value based on the observation introduces state into the evaluation, doesn't it? – immibis Oct 27 '16 at 23:43
20

First of all, let's clear up some misconceptions. There is no "bottom value". The bottom type is defined as a type that is a subtype of every other type in the language. From this, one can prove (in any interesting type system at least), that the bottom type has no values - it is empty. So there is no such thing as a bottom value.

Why is the bottom type useful? Well, knowing that it's empty let's us make some deductions on program behavior. For example, if we have the function:

def do_thing(a: int) -> Bottom: ...

we know that do_thing can never return, since it would have to return a value of type Bottom. Thus, there are only two possibilities:

  1. do_thing does not halt
  2. do_thing throws an exception (in languages with an exception mechanism)

Note that I created a type Bottom which does not actually exist in the Python language. None is a misnomer; it is actually the unit value, the only value of the unit type, which is called NoneType in Python (do type(None) to confirm for yourself).

Now, another misconception is that functional languages do not have exception. This isn't true either. SML for example has a very nice exception mechanism. However, exceptions are used much more sparingly in SML than in e.g. Python. As you've said, the common way to indicate some kind of failure in functional languages is by returning an Option type. For example, we would create a safe division function as follows:

def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None

Unfortunately, since Python doesn't actually have sum types, this isn't a viable approach. You could return None as a poor-man's option type to signify failure, but this is really no better than returning Null. There is no type-safety.

So I would advise following the language's conventions in this case. Python uses exceptions idiomatically to handle control flow (which is bad design, IMO, but it's standard nonetheless), so unless you're only working with code you wrote yourself, I'd recommend following standard practice. Whether this is "pure" or not is irrelevant.

  • By "bottom value" I actually meant the "bottom type", that's why I wrote that None didn't comply with the definition. Anyway, thanks for correcting me. Don't you think that using exception only to stop execution entirely or to return an optional value is okay with Python's principles? I mean, why is it bad to restrain from using exception for complicated control? – Eli Korvigo Oct 27 '16 at 14:50
  • @EliKorvigo That's what more or less what I said, right? Exceptions are idiomatic Python. – gardenhead Oct 27 '16 at 14:59
  • 1
    For example, I discourage my undergrad students to use try/except/finally like another alternative to if/else, i.e. try: var = expession1; except ...: var = expression 2; except ...: var = expression 3..., though it is a common thing to do in any imperative language (btw, I strongly discourage using if/else blocks for this as well). Do you mean, that I'm being unreasonable and should allow such patterns since "this is Python"? – Eli Korvigo Oct 27 '16 at 15:07
  • @EliKorvigo I agree with you in general (btw, you're a professor?). try... catch... should not be used for control flow. For some reason, that's how the Python community decided to do things though. For example, the safe_div function I wrote above would usually be written try: result = num / div: except ArithmeticError: result = None. So if you're teaching them general software engineering principles, you should definitely discourage this. if ... else... is also a code smell, but that's too long to go into here. – gardenhead Oct 27 '16 at 15:46
  • 2
    There is a "bottom value" (it's used in talking about the semantics of Haskell, for example), and it has little to do with the bottom type. So that isn't really a misconception of the OP, just talking about a different thing. – Ben Oct 28 '16 at 5:41
11

Since there has been so much interest in purity over the last few days, why don't we examine what a pure function looks like.

A pure function:

  • Is referentially-transparent; that is, for a given input, it will always produce the same output.

  • Does not produce side-effects; it doesn't change the inputs, outputs, or anything else in its external environment. It only produces a return value.

So ask yourself. Does your function do anything but accept an input and return an output?

  • 3
    So, it doesn't matter, how ugly it is written inside as long as it behaves functionally? – Eli Korvigo Oct 27 '16 at 14:53
  • 8
    Correct, if you're merely interested in purity. There might, of course, be other things to consider. – Robert Harvey Oct 27 '16 at 14:53
  • 3
    To play the devils advocate, I'd argue that throwing an exception is just a form of output and functions that throw are pure. Is that a problem? – Bergi Oct 27 '16 at 20:57
  • 1
    @Bergi I don't know that you're playing devil's advocate, since that's precisely what this answer implies :) The problem is that there are other considerations besides purity. If you allow unchecked exceptions (which by definition are not part of the function's signature) then the return type of every function effectively becomes T + { Exception } (where T is the explicitly declared return type), which is problematic. You cannot know whether a function will throw an exception without looking at its source code, which makes writing higher order functions problematic as well. – Andres F. Oct 27 '16 at 22:54
  • 2
    @Begri while throwing might be arguably pure, IMO using exceptions does more than just complicate the return type of each function. It damages the composability of functions. Consider the implementation of map : (A->B)->List A ->List B where A->B can error. If we allow f to throw an exception map f L will return something of type Exception + List<B>. If instead we allow it to return an optional style type map f L will instead return List<Optional<B>>`. This second option feels more functional to me. – Michael Anderson Oct 28 '16 at 1:55
7

Haskell semantics uses a "bottom value" to analyse the meaning of Haskell code. It's not something you really use directly in programming Haskell, and returning None is not at all the same kind of thing.

The bottom value is the value ascribed by Haskell semantics to any computation that fails to evaluate to a value normally. One such way a Haskell computation can do that is actually by throwing an exception! So if you were trying to use this style in Python, you actually should just throw exceptions as normal.

Haskell semantics uses the bottom value because Haskell is lazy; you're able to manipulate "values" that are returned by computations that haven't actually run yet. You can pass them to functions, stick them in data structures, etc. Such an unevaluated computation might throw an exception or loop forever, but if we never actually need to examine the value then the computation will never run and encounter the error, and our overall program might manage to do something well-defined and finish. So without wanting to explain what Haskell code means by specifying the exact operational behaviour of the program at runtime, we instead declare such erroneous computations produce the bottom value, and explain what that value behaves; basically that any expression which needs to depend on any properties at all of the bottom value (other than it existing) will also result in the bottom value.

To remain "pure" all possible ways of generating the bottom value have to be treated as equivalent. That includes the "bottom value" that represents an infinite loop. Since there's no way to know that some infinite loops actually are infinite (they might finish if you just run them for a bit longer), you can't examine any property of a bottom value. You can't test whether something is bottom, can't compare it to anything else, can't convert it to a string, nothing. All you can do with one is put it places (function parameters, part of a data structure, etc) untouched and unexamined.

Python already has this kind of bottom; it's the "value" you get from an expression that throws an exception, or doesn't terminate. Because Python is strict rather than lazy, such "bottoms" can't be stored anywhere and potentially left unexamined. So there's no real need to use the concept of the bottom value to explain how computations that fail to return a value can still be treated as if they had a value. But there's also no reason you couldn't think this way about exceptions if you wanted.

Throwing exceptions is actually considered "pure". It's catching exceptions that breaks purity - precisely because it allows you to inspect something about certain bottom values, instead of treating them all interchangeably. In Haskell you can only catch exceptions in the IO that allows impure interfacing (so it usually happens at a fairly outer layer). Python doesn't enforce purity, but you can still decide for yourself which functions are part of your "outer impure layer" rather than pure functions, and only allow yourself to catch exceptions there.

Returning None instead is completely different. None is a non-bottom value; you can test if something is equal to it, and the caller of the function that returned None will continue to run, possibly using the None inappropriately.

So if you were thinking of throwing an exception and want to "return bottom" to emulate Haskell's approach you just do nothing at all. Let the exception propagate. That's exactly what Haskell programmers mean when they talk about a function returning a bottom value.

But that's not what functional programmers mean when they say to avoid exceptions. Functional programmers prefer "total functions". These always return a valid non-bottom value of their return type for every possible input. So any function that can throw an exception isn't a total function.

The reason we like total functions is that they are much easier to treat as "black boxes" when we combine and manipulate them. If I have a total function returning something of type A and a total function that accepts something of type A, then I can call the second on the output of the first, without knowing anything about the implementation of either; I know I'll get a valid result, no matter how the code of either function is updated in future (as long as their totality is maintained, and as long as they keep the same type signature). This separation of concerns can be an extremely powerful aid for refactoring.

It's also somewhat necessary for reliable higher order functions (functions that manipulate other functions). If I want to write code that receives a completely arbitrary function (with a known interface) as a parameter I have to treat it as a black box because I have no way of knowing which inputs might trigger an error. If I'm given a total function then no input will cause an error. Similarly the caller of my higher order function won't know exactly what arguments I use to call the function they pass me (unless they want to depend on my implementation details), so passing a total function means they don't have to worry about what I do with it.

So a functional programmer that advises you to avoid exceptions would prefer you instead return a value that encodes either the error or a valid value, and requires that to use it you are prepared to handle both possibilities. Things like Either types or Maybe/Option types are some of the simplest approaches to do this in more strongly typed languages (usually used with special syntax or higher order functions to help glue together things that need an A with things that produce a Maybe<A>).

A function that either returns None (if an error happened) or some value (if there was no error) is following neither of the above strategies.

In Python with duck typing the Either/Maybe style is not used very much, instead letting exceptions be thrown, with tests to validate that the code works rather than trusting functions to be total and automatically combinable based on their types. Python has no facility for enforcing that code uses things like Maybe types properly; even if you were using it as a matter of discipline you need tests to actually exercise your code to validate that. So the exception/bottom approach is probably more suited to pure functional programming in Python.

  • 1
    +1 Awesome answer! And very comprehensive as well. – Andres F. Oct 28 '16 at 15:36
6

As long as there are no externally visible side effects and the return value depends exclusively on the inputs, then the function is pure, even if it does some rather impure things internally.

So it really depends on what exactly can cause exceptions to be thrown. If you're trying to open a file given a path, then no, it's not pure, because the file may or may not exist, which would cause the return value to vary for the same input.

On the other hand, if you're trying to parse an integer from a given string and throwing an exception if it fails, that could be pure, as long as no exceptions can bubble out of your function.

On a side note, functional languages tend to return the unit type only if there is just a single possible error condition. If there are multiple possible errors, they tend to return an error type with information about the error.

  • You can't return bottom type. – gardenhead Oct 27 '16 at 16:48
  • 1
    @gardenhead You're right, I was thinking of the unit type. Fixed. – 8bittree Oct 27 '16 at 16:54
  • In the case of your first example, isn't the filesystem and what files exist within it simply one of the inputs to the function? – Vality Oct 27 '16 at 19:07
  • 2
    @Vaility In reality you probably have a handle or a path to the file. If function f is pure, you'd expect f("/path/to/file") to always return the same value. What happens if the actual file gets deleted or changed between two invocations to f? – Andres F. Oct 27 '16 at 23:01
  • 2
    @Vaility The function could be pure if instead of a handle to the file you passed it the actual content (i.e. the exact snapshot of bytes) of the file, in which case you can guarantee that if the same content goes in, the same output goes out. But accessing a file is not like that :) – Andres F. Oct 27 '16 at 23:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.