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:
E.Either<t.Error, dbFnReturningUser | dbFnReturningGroup>
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.