26

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
  • @AndresF. I might be wrong, but isn't it because observing exceptions introduces state with the control flow? – Eli Korvigo Oct 27 '16 at 20:27
  • 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
  • @EliKorvigo "Introduces state with the control flow" is not a phrase that makes sense to me. – user253751 Oct 27 '16 at 22:00
  • @immibis Excuse me my bad wording (I'm not a native English speaker). I've meant, that observing a an exception and setting a variable's value based on the observation introduces state into the evaluation, doesn't it? – Eli Korvigo Oct 27 '16 at 22:36
  • @EricLippert If I understand you right, I've already done that for the sake of practice, though I've never seen that in any real Python code. I've added the my old attempt on this to the question (a usage example is provided in the doctest). – Eli Korvigo Oct 27 '16 at 22:52
  • 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? – user253751 Oct 27 '16 at 23:43
  • 1
    @immibis I've believed, that as long as you treat variables as constants, the evaluation is stateless, because the final output can be expressed as a long composition of functions (and intermediate return values stored in constants serve nothing but syntactic simplification and clarification). – Eli Korvigo Oct 27 '16 at 23:46
  • @EliKorvigo This misunderstanding is arising because you and immibis are using two different definitions of "variables" (which is unfortunately overloaded in mathematics). To distinguish the two, we can call "variables" whose contents may change an assignable and "variables" whose value cannot change identifiers. Then there is no (or at least less) confusion. – gardenhead Oct 28 '16 at 00:22
  • @EliKorvigo Under that (also perfectly valid) philosophy, setting a variable based on whether an exception occurred is not introducing state as long as you don't set it more than once, is it? Just like setting a variable from a function's return value is not introducing state as long as you don't set it more than once. Note that in try: ... exceptionOccurred=False except SomeError: exceptionOccurred=True exceptionOccurred is never set more than once. – user253751 Oct 28 '16 at 00:31
  • @immibis I agree with you in this context. What bothers me, though, is that if an unspecified exception will be raised and suppressed by an outer function, a variable may become undefined. Isn't it stateful? – Eli Korvigo Oct 28 '16 at 01:14
  • @EliKorvigo Why is the outer function looking at the values of variables that haven't been defined? – user253751 Oct 28 '16 at 01:27
  • @immibis Consider this case: ` def fn1(*args): try: var = ... except Error1: var = ... except Error2: var = ...
    return var
    
    

    def fn2(*args): try: var = fn1(...) ... except All_Errors as err: var = err return var Would you say that callingfn2` is stateful (I would only say that its return type is undefined)?

    – Eli Korvigo Oct 28 '16 at 01:48
  • 1
    @EliKorvigo I would say that asking whether a function call is stateful makes no sense. – user253751 Oct 28 '16 at 01:54
  • @immibis thank you for your time, you gave me some valuable information to digest. – Eli Korvigo Oct 28 '16 at 02:07

4 Answers4

22

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.

gardenhead
  • 4,747
  • 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
  • I'm a PhD student. Carrying out seminars and preaching to the undergrads under my command are some of my responsibilities. – Eli Korvigo Oct 27 '16 at 16:24
  • @EliKorvigo - Python uses baked-in exceptions for control flow - raising and catching StopIteration to exit for loops. Given a choice, I'd rather push down exception handling into implementation details, but where it's not possible, or preferable to avoid race conditions, I don't shy from using them. - See Raymond Hettinger's answer here that specifically addresses this issue: http://stackoverflow.com/questions/16138232/is-it-a-good-practice-to-use-try-except-else-in-python – Aaron Hall Oct 27 '16 at 21:06
  • @AaronHall I know that Python throws exceptions behind the scene all the time. Python does many impure things, even map/filter/reduce are stateful inside. But we still have the choice to avoid explicit state in our own programs by limiting it to IO processes and using primitives (such as map/filter or functions from different FP packages) to encapsulate state. – Eli Korvigo Oct 27 '16 at 21:35
  • Minor note -- if you use Python's optional gradual type system (e.g. PEP 484), then you do get sum types via Union. There's also the Optional type. Of course, it's complying by whatever type annotations are written down is optional, but if you do typecheck your code using tools like mypy, it's possible to have nice things like sum types and statically verifying that a value cannot be None. – Michael0x2a Oct 27 '16 at 23:41
  • @Michael0x2a I actually use MyPy. It does have a Union type, but it doesn't give sum types the proper behavior, because there is still no case construct in the language. – gardenhead Oct 28 '16 at 00:03
  • 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 05:41
  • @Ben Ah, you're correct. But that wasn't what the OP was talking about (I think). One term comes from type theory, the other from denotational semantics, and this is definitely a question on the former. – gardenhead Oct 28 '16 at 12:59
  • Are you sure? "Instead an erroneous calculation should be evaluated as a bottom value" sounds a lot like the use of bottom values in denotational semantics, and not very like the bottom type. – Ben Oct 28 '16 at 21:47
  • @Ben I'm not sure. We'll have to ask the OP what he meant. – gardenhead Oct 28 '16 at 22:55
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?

Robert Harvey
  • 199,517
  • 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
  • 1
    @AndresF. I was not sure what Robert wanted to imply, I could've argued for the other side as well (exceptions being not an ouput but rather non-termination). Of course you're right, the real side-effect of exceptions is that they change the type of every function in the project if only one function throws. Which makes reasoning for otherwise simple cases unnecessarily harder, and of course explicit is better than implicit. – Bergi Oct 27 '16 at 23:02
  • 3
    @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>`. This second option feels more functional to me. – Michael Anderson Oct 28 '16 at 01:55
10

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.

Ben
  • 1,027
  • 7
  • 10
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.

8bittree
  • 5,656
  • 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