Error handling and Functional Programming
paris.py 2/12/2019
About me
- @ThierryCols
- 🐍 2 to 3 years
- 🖥 4 years
Started functional stuff @inato 💊
Why we have exceptions
A simple problem
timestamp,content,viewed,href
2019-12-01T05:33:34Z,@guidovr is not onto functional shenanigans,unread,https://example.com/guido/the/boss
2019-11-30T13:47:12Z,@javascript mentioned you in 'Are you leaving me?' discussion,viewed,https://example.com/discussions/heartbroken
2019-11-29T03:50:08Z,@parispy sent you a functional poke,unread,https://example.com/interactions/poke/
CSV Input
[
{
"message": "@guidovr is not onto functional shenanigans",
"date": "Dimanche 1er Décembre",
"timestamp": "2019-12-01T05:33:34+00:00",
"link": "https://example.com/guido/the/boss",
"read": false
},
{
"message": "@javascript mentioned you in 'Are you leaving me?' discussion",
"date": "Samedi 30 Octobre",
"timestamp": "2019-11-30T13:47:12+00:00",
"link": "https://example.com/discussions/heartbroken",
"read": false
},
{
"message": "@parispy sent you a functional poke",
"date": "Vendredi 29 Octobre",
"timestamp": "2019-11-29T03:50:08+00:00",
"link": "https://example.com/interactions/poke/",
"read": true
}
]
JSON Output
What we'll usually do
def split_fields(row):
return row.split(",")
Get data (don't do it at home)
Build message from line
def zip_row(headers, row):
if len(headers) != len(row):
raise Exception("Row has unexpected number of fields")
return dict(zip(headers, row))
What we'll usually do
Return correct date
def add_human_readable_date(message):
error_message = "Unable to parse date stamp in message object"
months = [
"January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"
]
try:
date = datetime.strptime(message["timestamp"], "%Y-%m-%dT%H:%M:%SZ")
human_readable_date = "{day} {month} {year}".format(
day=date.day, month=months[date.month - 1], year=date.year
)
complete_message = dict(message)
complete_message["human_date"] = human_readable_date
return complete_message
except Exception:
raise Exception(error_message)
What we'll usually do
Format to output
def format_to_output(message):
return {
"message": message["content"],
"date": message["human_date"],
"timestamp": message["timestamp"],
"link": message["href"],
"read": True if message["viewed"] == "read" else False,
}
Format to error
def format_to_error(error_message):
return {"error": error_message}
What we'll usually do
Process a line
def process_line(headers, line):
try:
split_headers = split_fields(headers)
split_line = split_fields(line)
dict_line = zip_row(split_headers, split_line)
dict_line_with_date = add_human_readable_date(dict_line)
return format_to_output(dict_line_with_date)
except Exception as e:
return format_to_error(e.error_message)
Exceptions are good !
Nice separation of "happy path" and "error path"
Each function is responsible for its own failures
All exceptions happen in one place
Exceptions are good !
def process_line_with_no_exception(headers, line):
split_headers = split_fields(headers)
split_line = split_fields(line)
dict_line = zip_row(split_headers, split_line)
if dict_line is None:
return format_to_error("wrong number of items in row")
dict_line_with_date = add_human_readable_date(dict_line)
if dict_line_with_date is None:
return format_to_error("incorrect date in row")
return format_to_error(e.error_message)
Exceptions are too good !
Too good at "masking" errors
Hard to know where to try/except
Make functions not pure
Science to the rescue!
Having clearer functions
They shall always return something.
We could return Errors instead of None...
Result: same if/else pasta code
Polymorphism
One interface, several behaviors
class MyClass:
@staticmethod
def __str__():
return "cool polymorphism"
if __name__ == "__main__":
my_class = MyClass()
print(my_class) # ➡️ 'cool polymorphism'
print([1, 2]) # ➡️ '[1, 2]'
Let's build paths
class Left:
def __init__(self, value):
self.value = value
def run_function_on_happy_path(self, fn):
return
def __str__(self):
return "Left({})".format(str(self.value))
class Right:
def __init__(self, value):
self.value = value
def run_function_on_happy_path(self, fn):
return fn(self.value)
def __str__(self):
return "Right({})".format(str(self.value))
Let's build paths
if __name__ == "__main__":
def print_stuff(value):
print(value)
return value
left = Left("test")
left.run_function_on_happy_path(print_stuff) # prints nothing
print(left) # prints "Left(test)"
right = Right("test")
right.run_function_on_happy_path(print_stuff) # prints "test"
print(right) # prints "Right(test)"
Introducing map 🗺
class Left:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return self
def __str__(self):
return "Left({})".format(str(self.value))
class Right:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return Right(fn(self.value))
def __str__(self):
return "Right({})".format(str(self.value))
Introducing map 🗺
if __name__ == "__main__":
def plus1(x):
return x + 1
left = Left(1)
left.fmap(plus1).fmap(print_stuff) # prints nothing
right = Right(1)
right.fmap(plus1).fmap(print_stuff) # prints Right(2)
Path Driven Development
Left + Right = Either
Refactoring the code
Let's rewrite our functions
def zip_row(headers):
def zip_row(row):
length_match = len(headers) != len(row)
return (
Right(dict(zip(headers, row)))
if length_match
else Left(Exception("Row has unexpected number of fields"))
)
return zip_row
And rewrite our program
from either import Left, Right
def process_line(headers, line):
split_headers = split_fields(headers)
either_split_line = Right(line).fmap(split_fields)
either_dict_line = either_split_line.fmap(zip_row(split_headers))
# ... wait...
split_fields ➡️ value
zip_row ➡️ Either
What is fmap(Either)? 🤯
Now we chain ⛓
class Left:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return self
def chain(self, fn):
return self
def __str__(self):
return "Left({})".format(
str(self.value)
)
class Right:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return Right(fn(self.value))
def chain(self, fn):
return fn(self.value)
def __str__(self):
return "Right({})".format(
str(self.value)
)
Now we chain ⛓
And we get a cleaner program
from either import Left, Right
def process_line(headers, line):
split_headers = split_fields(headers)
either_split_line = Right(line).fmap(split_fields)
either_dict_line = either_split_line.chain(zip_row(split_headers))
either_dict_line_with_date = either_dict_line.chain(add_human_readable_date)
And how to return data?
eventually returning value
class Left:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return self
def chain(self, fn):
return self
def fold(self, on_left, on_right):
return on_left(self.value)
def __str__(self):
return "Left({})".format(
str(self.value)
)
class Right:
def __init__(self, value):
self.value = value
def fmap(self, fn):
return Right(fn(self.value))
def chain(self, fn):
return fn(self.value)
def fold(self, on_left, on_right):
return on_right(self.value)
def __str__(self):
return "Right({})".format(
str(self.value)
)
And we're done 🏁
from either import Left, Right
def process_line(headers, line):
split_headers = split_fields(headers)
return Right(line)
.fmap(split_fields)
.chain(zip_row(split_headers))
.chain(add_human_readable_date)
.fold(on_left=format_to_error, on_right=format_to_output)
A Labyrinthine System
Yes but,
Throwing an exception makes another function responsible for error handling
either is honest about letting you stay on the happy path
In the end we did not write much code to handle errors with our either
works super great with types
Cheers 🍻
Questions?
I work for inato
either
By Thierry Colsenet
either
- 1,167