the-frey~/blog

fp-ts and Do

Some musings on using Do notation in Typescript

27 November 2020

After a brief spike, we’ve started converting over most of our handlers to use the Do notation that’s included in the fp-ts-contrib package.

There’s an overview of do notation here, or Paul Gray’s much more comprehensive summary available here.

In a nutshell, most of our operations are asynchronous, so in moving to a monad-based implementation, we were going to be doing a lot of lifting already written async functions into a monadic context. In fp-ts terms, async operations are governed by Task. A Task that might fail is a TaskEither and helpfully it’s trivial to lift an Either into the TaskEither context. Assuming you have TaskEither imported as TE, it’s as simple as TE.fromEither(<your-async-fn(args)>).

To give the example of an endpoint that returned a User stripped of any PII or sensitive data, we came to this in our initial spike:

While my friend that is a co-organiser of Lambda Lounge MCR pointed out that if you don’t care about the intermediate binding steps in the Do notation, the following is basically equivalent:

You’ll notice some of the stuff in this example isn’t typed - our events and whatnot are actually typed in the codebase with something less aggressive than the AWS/types package so that we can pass minimal request maps from integration tests and treat our handlers like simple functions that accept a hashmap as input - see my other blog post for some more info about how this thrashes out in practice.

From there, we realised that handling the database connection would be more effectively done using a Reader. Luckily, there’s a ReaderTaskEither in the package.

An example database function for us looks something like this:

You’ll notice that the error type and the entity type in these examples all follow a similar shape to an HTTP response. Originally we had an HTTP response type with error|success subtypes as the left|right branches in an Either, but it felt weird for so many parts of the stack to have ‘knowledge’ of that type. We ended up replacing it with a generic hashmap type with an optional status code in the error case.

Although these types are mostly equivalent to an HTTP response type, they’re fractionally more generic, and we can use them elsewhere. Hey, it’s a work-in-progress.

If Typescript (and Javascript) were better at handling arrays, we might have coined a variant of [type entity] to describe these, but in the TS world an interface to govern a hashmap is easier.

We’re gradually enumerating all the allowed types in the top entity key, but it’s taking a while to catch all the possibilities, so even in the real codebase it has an any fallback. Which… isn’t great, but we have to compromise for the sake of continuing to deliver features at a reasonable pace as we re-write.

After that, we refactored out the db conn into a System map, which would allow us to pass around env and other connections as well in the Reader:

Which we could then use like so:

Finally, by using the side-effecting .do call (and .doL if bindings are needed), we’re able to open transactions over calls, resolving them in the final fold operation.

These functions use the Reader context that’s already been used for the other operations, and just return the op inside (in this case, a response).

This means that you can get to a very reusable place where you’re able to use nested bindings and fluently control your error states and validation steps like slotting together Lego.

The types on line 25 get pretty crazy, but you can always break these out into their own line with a type declaration if they become either:

Interestingly, the case for unions is usually that the function returns a function - such as the case where the arity might differ, for a case in which a database function selects by multiple ids depending on context:

This allows you to go E.Either<t.Error, idDbFn | idEntDbFn> which is a bit more readable in the longer run I think.

As I’ve worked with the cats library and its mlet form in the past, this style of almost lisp/let style binding really appeals to me as a way of composing behaviour in a really neat and terse way.

Fork me on GitHub