paris.py 2/12/2019
Started functional stuff @inato 💊
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
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))
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)
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}
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)
Nice separation of "happy path" and "error path"
Each function is responsible for its own failures
All exceptions happen in one place
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)
Too good at "masking" errors
Hard to know where to try/except
Make functions not pure
They shall always return something.
We could return Errors instead of None...
Result: same if/else pasta code
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]'
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))
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)"
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))
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)
Left + Right = Either
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
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)? 🤯
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)
)
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?
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)
)
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)
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