Now, let's try out returns.result library (v0.11.0 at the time of writing), clearly inspired by Haskell's Either monad and do notation. I'm quite glad someone already implemented it and I didn't have to reinvent the wheel here.

So, let's try and rewrite the code using returns.result.Result :

19: from returns.result import safe 20: 21: @safe 22: def parse_entry (entry: str ) -> Highlight: 23: groups = re.search( 24: r '(?P<title>.*)$

.*Highlight on Page (?P<page>\d+).*Added on (?P<dts>.*)$



(?P<text>.*)$' , 25: entry, 26: re.MULTILINE, 27: ) 28: assert groups is not None , "Couldn't match regex!" 29: dt = datetime.strptime(groups[ 'dts' ], '%A, %B %d, %Y %I:%M:%S %p' ) 30: return Highlight( 31: dt=dt, 32: title=groups[ 'title' ], 33: page=groups[ 'page' ], 34: text=groups[ 'text' ], 35: ) 36: 37: from returns.result import Result 38: from typing import Iterator 39: def iter_highlights () -> Iterator[Result[Highlight, Exception ]]: 40: data = Path(clippings_file).read_text() 41: for entry in data.split( '==========' ): 42: yield parse_entry(entry.strip())

So far the only difference from the original code is @safe decorator on parse_entry , which basically deals with catching all exceptions and wrapping into Result .

As a consequence, iter_highlights required no changes in its body. (which may not be a desirable thing as we'll see later)

43: from typing import cast 44: from returns.result import Success, Failure 45: from itertools import tee 46: def iter_books () -> Iterator[Result[Book, Exception ]]: 47: vit , eit = tee(iter_highlights()) 48: sentinel = cast(Highlight, object ()) 49: values = (r.unwrap() for r in vit if r.value_or(sentinel) is not sentinel) 50: errors = (r.failure() for r in eit if r.value_or(sentinel) is sentinel) 51: key = lambda e: e.title 52: for book, hls in groupby( sorted (values, key=key), key=key): 53: highlights = list ( sorted (hls, key= lambda hl: hl.dt)) 54: yield Success(Book(title=book, highlights=highlights)) 55: for e in errors: 56: yield Failure(e)

Ok, that definitely requires some explanation…

returns library public API doesn't provide any way to tell between success and failure (kind of deliberately). The types _Success and _Failure are private, and the only method that we can use seems to be result.value_or(default) . This method returns the success value if result is Success and falls back to default if result is a Failure . So we use a sentinel object to distinguish between actual success values and default ones, and also have to trick mypy with a cast .

Apart from this obscurity, the function suffers from exactly the same issues as the iter_books implementation from the previous section, and for the same reason: contract is too complicated to be expressed in mypy.

One could argue that this function is going to look awkward anyway since we need to separate list of results into successes and errors. Let's see the function that should be more straightforward:

57: from typing import Callable 58: def print_books () -> None : 59: for r in iter_books(): 60: def print_ok (r: Book) -> None : 61: print (f '* {r.title}' ) 62: for h in r.highlights: 63: text = "

" .join(wrap(h.text)) 64: print (f ' - {h.dt:%d %b %Y %H:%M} {text} [Page {h.page}]' ) 65: print_error = lambda e: print (f "* ERROR: {e}" ) 66: r. map (print_ok).fix(print_error)

The idea here is that we can use map method (that works like fmap in Haskell) and use it to print successful results, and chain it with fix that works like like fmap , but for errors. In a sense, these methods encapsulate pattern matching (which Python lacks syntactically) so as long the implementor did the dirty business of correctly doing it dynamically, you're safe. However I feel that this particular library overdid this encapsulation a bit, hence very hacky implementation of iter_books .

Lambdas can't be multiline, so we have to define a local function for print_ok .

There is a bug in mypy that sometimes prevents you from inlining the lambda and struggles with type inference. Here I'm hitting this bug with print_error , that's why it's not .fix(lambda e: print(f"* ERROR: {e}")) .

Another potential problem is one could forget to implement one of map/fix clauses, since nothing enforces calling them. Even if you're detecting unused variables, missing .fix clause could stay unnoticed forever. It's very similar to forgetting catch when using Javascript Promises.

It might be possible to enforce with some static analysis though, e.g. via mypy plugin by flagging dangling/temporary Result values (e.g. similarly to must_use attribute in Rust), but it's a project on its own.

Well at the very least it works and type checks!

67: print_books()

Python output [exit code 0]: * PHYS771 Lecture 12: Proof (scottaaronson.com) - 21 Jul 2013 10:06 Roger Penrose likes to talk about making direct contact with Platonic reality, but it's a bit embarrassing when you think you've made such contact and it turns out the next morning that you were wrong! [Page 2] - 04 Aug 2013 20:41 No hidden-variable theory can be local (I think some guy named Bell proved that). [Page 14] * [Tong][2013] Dynamics and Relativity - 04 Aug 2013 18:17 It is worth mentioning that although the two people disagree on whether the light hits the walls at the same time, this does not mean that they can't be friends. [Page 120] * ERROR: Couldn't match regex! Mypy output [exit code 0]: Success: no issues found in 1 source file

Overall I'm not sold, Python simply lacks syntax that lets you unpack and compose Result objects in a clean way and you end up with boilerplate. lifts are not very readable in Haskell, let alone in Python.

I think authors did a great experiment though, the more people have fun with types, the more good abstractions we'll find.

I don't want to discourage people from using their library, so if it's your personal project and it makes your code more manageable or it just feels fun then by all means go for it!

But as much as I like ideas from functional programming, I'm almost certain that it's gonna look confusing to an average Python programmer, and won't be welcome warmly in your team.