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
Best Answer
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:
we know that
do_thing
can never return, since it would have to return a value of typeBottom
. 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 calledNoneType
in Python (dotype(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: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 returningNull
. 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.