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
-
1Your 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.– Andres F.2016年10月27日 18:42:56 +00:00Commented Oct 27, 2016 at 18:42
-
6There 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– Eric Lippert2016年10月27日 19:59:45 +00:00Commented Oct 27, 2016 at 19:59
-
3No, 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.– Andres F.2016年10月27日 20:51:15 +00:00Commented Oct 27, 2016 at 20:51
-
33- 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.– Andres F.2016年10月27日 20:53:21 +00:00Commented Oct 27, 2016 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?Stack Exchange Broke The Law– Stack Exchange Broke The Law2016年10月27日 23:43:09 +00:00Commented Oct 27, 2016 at 23:43
4 Answers 4
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:
do_thing
does not haltdo_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– Eli Korvigo2016年10月27日 14:50:17 +00:00Commented Oct 27, 2016 at 14:50 -
@EliKorvigo That's what more or less what I said, right? Exceptions are idiomatic Python.gardenhead– gardenhead2016年10月27日 14:59:31 +00:00Commented Oct 27, 2016 at 14:59
-
1For example, I discourage my undergrad students to use
try/except/finally
like another alternative toif/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 usingif/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– Eli Korvigo2016年10月27日 15:07:42 +00:00Commented Oct 27, 2016 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, thesafe_div
function I wrote above would usually be writtentry: 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– gardenhead2016年10月27日 15:46:18 +00:00Commented Oct 27, 2016 at 15:46 -
2There 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– Ben2016年10月28日 05:41:52 +00:00Commented Oct 28, 2016 at 5:41
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?
-
3So, it doesn't matter, how ugly it is written inside as long as it behaves functionally?Eli Korvigo– Eli Korvigo2016年10月27日 14:53:20 +00:00Commented Oct 27, 2016 at 14:53
-
8Correct, if you're merely interested in purity. There might, of course, be other things to consider.Robert Harvey– Robert Harvey2016年10月27日 14:53:37 +00:00Commented Oct 27, 2016 at 14:53
-
3To 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– Bergi2016年10月27日 20:57:27 +00:00Commented Oct 27, 2016 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 }
(whereT
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.– Andres F.2016年10月27日 22:54:20 +00:00Commented Oct 27, 2016 at 22:54 -
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
whereA->B
can error. If we allow f to throw an exceptionmap f L
will return something of typeException + List<B>
. If instead we allow it to return anoptional
style typemap f L
will instead return List<Optional<B>>`. This second option feels more functional to me.Michael Anderson– Michael Anderson2016年10月28日 01:55:44 +00:00Commented Oct 28, 2016 at 1:55
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.– Andres F.2016年10月28日 15:36:55 +00:00Commented Oct 28, 2016 at 15:36
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– gardenhead2016年10月27日 16:48:58 +00:00Commented Oct 27, 2016 at 16:48
-
1@gardenhead You're right, I was thinking of the unit type. Fixed.8bittree– 8bittree2016年10月27日 16:54:42 +00:00Commented Oct 27, 2016 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– Vality2016年10月27日 19:07:08 +00:00Commented Oct 27, 2016 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 expectf("/path/to/file")
to always return the same value. What happens if the actual file gets deleted or changed between two invocations tof
?Andres F.– Andres F.2016年10月27日 23:01:03 +00:00Commented Oct 27, 2016 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.– Andres F.2016年10月27日 23:03:25 +00:00Commented Oct 27, 2016 at 23:03
Explore related questions
See similar questions with these tags.