A Trello Monad in the Dark
January 2016
We are ceaselessly reminded of life’s fragility as we watch every cell, body, and star expire, disintegrate, and fade. An unending confusion attends our senses as they helplessly seek agency in a senseless void. And the brevity of this nasty brutishness is its cruelest irony. We must conclude that life is a fragile, confusing, and brief glimpse of an incomprehensibly beautiful yet ultimately indifferent universe.
The universe contains one countervailing force, however, that can fortify fragility, blow back the fog of confusion, and buy us at least one more sunset with a lover, one more bedtime story with our children, one more proud moment with a venerated elder.
I am, of course, referring to PureScript–a purely functional, statically typed, compile-to-JavaScript programming language inspired by Haskell. I’d like to share my experience of creating a Trello API in PureScript, which ameliorated the fragility, confusion, and time-sucking inefficiencies of working with the official asynchronous JavaScript client.
Using the official Trello client, let’s write a JavaScript function that downloads the first two boards belonging to a given organization, processes the boards, and returns the results:
function processFirstAndSecondBoardForOrg(client, orgName, success, error) {
client.organizations.get(orgName, org => {
client.boards.get(org.boards[0].id, firstBoard => {
client.boards.get(org.boards[1].id, secondBoard => {
processBoards(client, [firstBoard, secondBoard], success, error);
}, error);
}, error);
}, error);
}
Even with this tiny, idealized example, and the niceties of es6, this is fragile, confusing, and time-sucking code. Setting aside the age-old arguments about static versus dynamic typing, manually passing continuations and credentials is an indirect and unpleasant way to express the intent here, and is harder to understand than its synchronous equivalent. It would be better if we could just write something like the following:
function processFirstAndSecondBoardForOrg(client, orgName) {
let org = client.organizations.get(orgName);
let firstBoard = client.boards.get(org.boards[0].id);
let secondBoard = client.boards.get(org.boards[1].id);
return processBoards(client, [firstBoard, secondBoard]);
}
And even make the client-passing implicit, through the use of a global or class variable:
function processFirstAndSecondBoardForOrg(orgName) {
let org = client.organizations.get(orgName);
let firstBoard = client.boards.get(org.boards[0].id);
let secondBoard = client.boards.get(org.boards[1].id);
return processBoards([firstBoard, secondBoard]);
}
Can PureScript allow us to use Trello this way? Here’s the actual PureScript equivalent*, which is even lovelier than the idealized JavaScript version (not to mention that it’s also statically typed). This code is just as asynchronous as the first example, and doesn’t use any global or mutable state:
processFirstAndSecondBoardForOrg orgName = do
org <- getOrg orgName
firstBoard <- getBoard (org.boards !! 0).id
secondBoard <- getBoard (org.boards !! 1).id
processBoards [firstBoard, secondBoard]
I’m going to explain how I designed this Trello API in PureScript. The explanation is very technical and likely beyond the immediate understanding of even experienced JavaScript developers, but I hope the powerful result and style of reasoning will entice you to explore PureScript further.
(* In both JavaScript and PureScript, I’ve elided array bounds checking to make the examples clearer. To be fair, arrays are intentionally cumbersome to use in PureScript to guarantee safety, and would make the PureScript solution look as complex as the JavaScript. To deal with this, a PureScript programmer would use higher-order functions beyond the scope of this post. I’ve chosen to simplify this aspect to focus on working with asynchronous APIs.)
Toward a Better Trello API
Let’s start with a naive translation of the JavaScript API into PureScript, then progressively refine it. Here are the PureScript types that more-or-less directly correspond to the JavaScript client, which we bind to the underlying JavaScript implementation. These are optional type definitions that we write down to reason about the API we want, before we implement it:
-- A Trello operation is a type alias for a function that takes...
type Trello a = forall e.
Client -- a credentialed client,
-> (Error -> Eff e Unit) -- an error callback,
-> (a -> Eff e Unit) -- a success callback,
-> Eff e Unit -- and returns an action that runs in the main context.
-- Some library functions we'll define elsewhere
getOrg :: String -> Trello Organization
getBoard :: Id -> Trello Board
-- A function of our own that consumes the API, defined elsewhere
processBoards :: Array Board -> Trello (Array Board)
Now we can write our first PureScript implementation of the original example:
processFirstAndSecondBoardForOrg :: String -> Trello (Array Board)
processFirstAndSecondBoardForOrg orgName client error success =
getOrg orgName client error (\org ->
getBoard (org.boards !! 0).id client error (\firstBoard ->
getBoard (org.boards !! 1).id client error (\secondBoard ->
processBoards [firstBoard, secondBoard] client error success
)
)
)
If you understand that PureScript writes function application f x y
where JavaScript writes f(x, y)
, you’ll see that this is very similar to the callback-based JavaScript example.
Dealing with Callbacks
Callback-based programming has been maligned as “this generation’s goto
statement” with good reason: it produces code that’s indirect, inverted, and hard to follow, making even basic flow control surprisingly difficult to reason about. When you write callback-based code, you’re doing work that the compiler should be doing. C# and TypeScript have, and there’s a proposal for es7 to include, async
/await
keywords that allow the compiler to transform synchronous-style code into continuation-passing style.
PureScript has a type Aff a e
with two corresponding functions for converting between values of type (Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit
:
-- Make an Aff e a from a callback-style function
makeAff :: forall e a. ((Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit) -> Aff e a
-- Run an Aff e a given error and success callbacks for the final result
runAff :: forall e a. (Error -> Eff e Unit) -> (a -> Eff e Unit) -> Aff e a -> Eff e Unit
This basically means that we can take any functions that previously required explicit error and success callbacks, and instead write them as Aff e a
values. We use this insight to rewrite our library functions getOrg
and getBoard
with calls to makeAff
, and change our Trello
type:
-- Previous type:
type Trello a = forall e. Client -> (Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit
-- Using Aff e a instead of (Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit:
type Trello a = forall e. Client -> Aff e a
And our example simplifies to
processFirstAndSecondBoardForOrg orgName client = do
org <- getOrg orgName client
firstBoard <- getBoard (org.boards !! 0).id client
secondBoard <- getBoard (org.boards !! 1).id client
processBoards [firstBoard, secondBoard] client
Aff
threads error and success callbacks throughout our Trello
operations so we don’t have to do it manually. Instead, we specify these callbacks only when we use runAff
to get the final result of an entire Trello
transaction.
Making the Client Implicit
The final bit of tedium is that we’re still manually passing a client instance around to getOrg
, getBoard
, and our own separate function, processBoards
. Enter ReaderT
, a function that transforms any r -> m a
(e.g. Client -> Aff e a
) into a computation that treats r
as an implicit, read-only parameter accessible via ask
:
-- A monad built from a function that takes an `r` and produces a monadic value
newtype ReaderT r m a = ReaderT (r -> m a)
-- Read the `r` within the monadic context
ask :: forall r m. ReaderT r m r
-- Run the ReaderT computation
runReaderT :: forall r m a. ReaderT r m a -> (r -> m a)
Now we can do the final refinement on our Trello API, making Trello
an alias for ReaderT
, giving us an implicit Client
on top of our asynchronous Aff
semantics:
-- Previous type:
type Trello a = forall e. Client -> Aff e a
-- Final type:
type Trello a = forall e. ReaderT Client (Aff e) a
-- `ask` becomes an API function that returns the current client
ask :: Trello Client
-- `runReaderT` lets us run the Trello computation as an async
-- computation that requires a Client
runReaderT :: forall e a. Trello a -> (Client -> Aff e a)
Now getOrg
and getBoard
can use ask
to get the current client, so we remove the manual client
passing:
processFirstAndSecondBoardForOrg orgName = do
org <- getOrg orgName
firstBoard <- getBoard (org.boards !! 0).id
secondBoard <- getBoard (org.boards !! 1).id
processBoards [firstBoard, secondBoard]
To run this code, we define runTrello
:
runTrello :: forall e a.
Trello a
-> Client
-> (Error -> Eff e Unit)
-> (a -> Eff e Unit)
-> Eff e Unit
runTrello trello client error success = runAff error success (runReaderT trello client)
Which does the following:
- Takes a
Trello a
, a client, an error callback, and a success callback. - Uses
runReaderT
to recover aClient -> Aff e a
from theTrello a
and calls it with the client. - Uses
runAff
to recover a(Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit
from theAff e a
and calls it with the error and success callbacks.
Conclusion
Honestly, I’m probably almost as confused as you are right now. I’ve never attempted to explain such a beefy bit of PureScript API design and it was much more involved than I expected! Luckily I’ve done the hardest part of implementing this API, which you can find on GitHub. I will continue to refine this explanation as I think it over. Enjoy and stay tuned!