Purescript: express, Aff, FS together

Something I continually find difficult in my struggle to become facile in purescript is combining effects, monads, etc with functionality from multiple services. While I do like the idea of fine effect control (stay tuned for my take on this in purescript-native, where IMO the gain is much greater), my unfamiliarity with haskell-like languages in general and purescript in particular often leaves me hedging by reaching for fable and F# or elm; the former because I have a long history with both O’Caml and .net and the latter because it’s just so damn pleasant.

I took a peek to see if I could just write a simple web service the other day in purescript, giving myself two hours before just doing it in F#. I ate two hours and then wrote it in F#.

I stuck it out this morning to figure it out:

What I had:

import Control.Monad.Eff.Class (liftEff)
resultJson :: Int -> { name :: String, times :: Int }
resultJson n dat =
{ name: “test”
, times: n
}
indexHandler :: forall e. Ref AppState -> Handler (err :: EXCEPTION, ref :: REF | e)
indexHandler stateRef =
let
getSt (AppState st) = st
in
do
state <- liftEff $ readRef stateRef
st <- liftEff $ pure $ getSt state
sendJson (resultJson st.count)

What I thought I wanted:

import Control.Monad.Eff.Class (liftEff)
import Node.FS as FS
import Node.FS.Aff as AIO
...
indexHandler :: forall e. Ref AppState -> Handler (err :: EXCEPTION, fs :: IO.FS, ref :: REF | e)
indexHandler stateRef =
let
getSt (AppState st) = st
in
do
state <- liftEff $ readRef stateRef
st <- liftEff $ pure $ getSt state
-- Handler is
HandlerM (Request -> Response -> Eff e Unit -> Aff e a)
-- So I think to myself, <- strips Aff ..., not thinking that
-- the monad decorating <- here is HandlerM (...)
-- but the type of HandlerM is more complicated than that.

fdata <- AIO.readTextFile Encoding.UTF8 “test.txt”
sendJson (resultJson st.count fdata)

What I figured out I needed:

import Control.Monad.Aff.Class (liftAff)
import Control.Monad.Eff.Class (liftEff)
import Node.FS as FS
import Node.FS.Aff as AIO
...
indexHandler :: forall e. Ref AppState -> Handler (err :: EXCEPTION, fs :: IO.FS, ref :: REF | e)
indexHandler stateRef =
let
getSt (AppState st) = st
in
do
state <- liftEff $ readRef stateRef
st <- liftEff $ pure $ getSt state
-- The answer turned out to be easier and different from
-- what I expected: liftAff replaces liftEff in the case of
-- an Aff, or asynchronous effect. This works because
-- HandlerM is based on Aff.

fdata <- liftAff $ AIO.readTextFile Encoding.UTF8 “test.txt”
sendJson (resultJson st.count fdata)

To explain something of what’s going on here:

HandlerM (Request -> Response -> Eff e Unit -> Aff e a)

Makes more sense to me written like this:

HandlerM ((Request -> Response -> Eff e Unit) -> Aff e a)

Now it starts to make sense; each HandlerM takes an express request handling function and yields an asynchronous promise chain that carries it out, such as what one might write in F#:

(* Note that I'm using some custom wrappers, but you can see that
* shape of the result is the same: this express handler returns
* a promise that eventually ends the response. *)
app |> Express.get “/”
(fun req res nxt ->
QIO.readTextFile "utf8" “test.txt”
|> Q.handle (fun e -> toString e)
|> Q.map (Express.resEndString res)
|> Q.fin
)

One of the more confusing aspects of this is that we don’t see where this function comes from. I often see things like this in Haskell code:

-- Does something with an a
data DoSomething a = DoSomething a

Objectively, this is literally a declaration of a type. It does nothing on its own to an a, let alone Something.

What’s missing here that I’ve had to get used to is the idea that the actual doing of the something is done by a typeclass instance, such as this code from purescript-express:

instance functorHandlerM :: Functor (HandlerM e) where
map f (HandlerM h) = HandlerM \req resp nxt ->
(h req resp nxt >>= \r -> pure $ f r)

Not only is code related to the handler embedded in the typeclass instance, but you can actually see that the req-resp-nxt function comes from the typeclass instance, not any code the end user has to see or handle.

HandlerM is making explicit the idea that the caller accepts that the request will be handled asynchronously. Without Aff in that final result, we wouldn’t be able to launch an asynchronous task and have it walk off with the EXPRESS effect.

So because of that, the liftEff applications above, despite seeming to come from Control.Monad.Eff.Class, actually comes from a place appropriate to the type involved. Control.Monad.Eff.Class just declares the typeclass method that others will implement based on the incoming types. In this case, an applicable liftEff exists for Aff so synchronous side effecting code can be used as normal in a promise chain, just as one would expect.

I hope this was valuable to somebody out there scratching their head about purescript in the future.