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