I don't think returning an error code is necessarily a bad thing; it's up to the programmer to decide which error handling strategy they want to employ. I had to review some Ruby code the other day and they'd returned a string in the error case, rather than throwing an exception. I think it's fine for quite simple cases and AFAIK Go does error handling that way too. Of course, if you need to do a lot of computations in a sequence, any of which could fail, then this simple strategy doesn't scale because the code becomes cluttered with all the
if
/
else
checks you have to do.
An alternative to exceptions is to use a result type (or "result monad") that represents a successful computation, or a failure. These can be chained together, resulting in code that's quite flat and readable and it's up to you when you actually do something with the failure. This is the functional approach, since in FP, you don't use exceptions as functions are pure and just return values. In Rust, it's called a
Result
, Scala has two types :
Try
and
Either
(my codebase at work actually uses both for whatever reason) and Kotlin has different library implementations (one of which is Nat Pryce's Result4k, from which I nicked the idea of the
result_from
function you see below).
Here are some test cases showing the idea, with an implementation following (note the code isn't meant for production; it's literally just for demo purposes. There are likely more fully featured implementations out there).
import unittest
from result import *
class TestResult(unittest.TestCase):
def test_a_computation_that_completes_is_a_success(self):
def a_computation_that_runs_successfully():
x = 5
return x * 10
self.assertEqual(
result_from(a_computation_that_runs_successfully),
Success(50)
)
def test_a_computation_that_throws_an_exception_is_a_failure(self):
def a_computation_that_fails():
return int("foobar")
self.assertEqual(
result_from(a_computation_that_fails),
Failure("invalid literal for int() with base 10: 'foobar'")
)
def test_composing_computations_runs_them_in_succession(self):
self.assertEqual(
Success(4)
.flatmap(lambda x: Success(x + 1))
.flatmap(lambda x: Success(x * 2)),
Success(10)
)
def test_no_computation_is_carried_out_after_a_failure(self):
self.assertEqual(
Success(4)
.flatmap(lambda x: Success(x + 1))
.flatmap(lambda x: Failure("something bad happened"))
.flatmap(lambda x: Success(x * 2)),
Failure("something bad happened")
)
def test_a_success_can_be_transformed_into_another(self):
self.assertEqual(
Success(4).map(lambda x: x + 1),
Success(5)
)
def test_transforming_a_failure_leaves_it_unchanged(self):
self.assertEqual(
Failure("something bad happened").map(lambda x: x + 1),
Failure("something bad happened")
)
if __name__ == '__main__':
unittest.main(verbosity=2)
class Result(object):
pass
class Success(Result):
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
def map(self, f):
return Success(f(self.value))
def flatmap(self, f):
return f(self.value)
def __eq__(self, other):
return self.value == other.value
class Failure(Result):
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
def map(self, f):
return Failure(self.value)
def flatmap(self, f):
return Failure(self.value)
def __eq__(self, other):
return self.value == other.value
def result_from(f):
try:
return Success(f())
except Exception as e:
return Failure(str(e))
I've kept the name of the method that lets you chain computations as
flatmap
as that's what it's called in the functional programming literature, since it really is analogous to
map
, but gets rid of any nesting (if you were to
map
, you'd end up with a
Result
inside another of course). Having said that, I did see a blog post where they'd called it
andThen
in a particular Kotlin implementation, because that made it more readable.
A
Result
is just a value, so at the point you need to know whether the computation succeeded or failed, you just check what kind of result you have:
result = some_computation_that_either_succeeds_or_fails()
if isinstance(result, Success):
print(f"It succeeded and the result was: {result.value}")
else:
print(f"It failed and the error was: {result.value}")
Other languages have pattern matching or otherwise more powerful versions of
switch
that make this look nicer, but the idea is the same.