purescript-aff
An asynchronous effect monad for PureScript.
The moral equivalent of ErrorT (ContT Unit (Eff e)) a
, for effects e
.
Aff
lets you say goodbye to monad transformers and callback hell!
Example
main = launchAff do
response <- Ajax.get "http://foo.bar"
liftEff $ log response.body
See the tests for more examples.
Getting Started
Installation
bower install purescript-aff
Introduction
An example of Aff
is shown below:
deleteBlankLines path = do
contents <- loadFile path
let contents' = S.join "\n" $ A.filter (\a -> S.length a > 0) (S.split "\n" contents)
saveFile path contents'
This looks like ordinary, synchronous, imperative code, but actually operates asynchronously without any callbacks. Error handling is baked in so you only deal with it when you want to.
The library contains instances for Semigroup
, Monoid
, Apply
, Applicative
, Bind
, Monad
, Alt
, Plus
, MonadPlus
, MonadEff
, and MonadError
. These instances allow you to compose asynchronous code as easily as Eff
, as well as interop with existing Eff
code.
Escaping Callback Hell
Hopefully, you're using libraries that already use the Aff
type, so you don't even have to think about callbacks!
If you're building your own library, or you have to interact with some native code that expects callbacks, then purescript-aff provides a makeAff
function:
makeAff :: forall e a. ((Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit) -> Aff e a
This function expects you to provide a handler, which should call a user-supplied error callback or success callback with the result of the asynchronous computation.
For example, let's say we have an AJAX request function that expects a callback:
exports.ajaxGet = function(callback) { // accepts a callback
return function(request) { // and a request
return function() { // returns an effect
doNativeRequest(request, function(response) {
callback(response)(); // callback itself returns an effect
});
}
}
}
foreign import ajaxGet :: forall e. (Response -> Eff e Unit) -> Request -> Eff e Unit
We can wrap this into an asynchronous computation like so:
ajaxGet' :: forall e. Request -> Aff e Response
ajaxGet' req = makeAff (\error success -> ajaxGet success req)
This eliminates callback hell and allows us to write code simply using do
notation:
do response <- ajaxGet' req
liftEff $ log response.body
Eff
All purely synchronous computations (Eff
) can be lifted to asynchronous computations with liftEff
defined in Control.Monad.Eff.Class
(see here).
import Control.Monad.Eff.Class
liftEff $ log "Hello world!"
This lets you write your whole program in Aff
, and still call out to synchronous code.
If your Eff
code throws exceptions (err :: Exception
), you can remove the exceptions using liftEff'
, which brings exceptions to the value level as an Either Error a
:
do e <- liftEff' myExcFunc
liftEff $ either (const $ log "Oh noes!") (const $ log "Yays!") e
Dealing with Failure
The Aff
monad has error handling baked in, so ordinarily you don't have to worry about it.
When you need to deal with failure, you have several options.
- Attempt
- Alt
- MonadError
1. Attempt
If you want to attempt a computation but recover from failure, you can use the attempt
function:
attempt :: forall e a. Aff e a -> Aff e (Either Error a)
This returns an Either Error a
that you can use to recover from failure.
do e <- attempt $ Ajax.get "http://foo.com"
liftEff $ either (const $ log "Oh noes!") (const $ log "Yays!") e
2. Alt
Because Aff
has an Alt
instance, you may also use the operator <|>
to provide an alternative computation in the event of failure:
do result <- Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
return result
3. MonadError
Aff
has a MonadError
instance, which comes with two functions: catchError
, and throwError
.
These are defined in purescript-transformers. Here's an example of how you can use them:
do resp <- (Ajax.get "http://foo.com") `catchError` (const $ pure defaultResponse)
if resp.statusCode != 200 then throwError myErr
else pure resp.body
Thrown exceptions are propagated on the error channel, and can be recovered from using attempt
or catchError
.
Forking
Using the forkAff
, you can "fork" an asynchronous computation, which means
that its activities will not block the current thread of execution:
forkAff myAff
Because Javascript is single-threaded, forking does not actually cause the computation to be run in a separate thread. Forking just allows the subsequent actions to execute without waiting for the forked computation to complete.
If the asynchronous computation supports it, you can "kill" a forked computation using the returned canceler:
canceler <- forkAff myAff
canceled <- canceler `cancel` (error "Just had to cancel")
_ <- liftEff $ if canceled then (log "Canceled") else (log "Not Canceled")
If you want to run a custom canceler if some other asynchronous computation is
cancelled, you can use the cancelWith
combinator:
otherAff `cancelWith` myCanceler
AVars
The Control.Monad.Aff.AVar
module contains asynchronous variables, which are very similar to Haskell's MVar
construct. These can be used as low-level building blocks for asynchronous programs.
do v <- makeVar
forkAff (later $ putVar v 1.0)
a <- takeVar v
liftEff $ log ("Succeeded with " ++ show a)
You can use these constructs as one-sided blocking queues, which suspend (if
necessary) on take
operations, or as asynchronous, empty-or-full variables.
Parallel Execution
There are MonadPar
and MonadRace
instances defined for Aff
, allowing for parallel execution of Aff
computations.
There are two ways of taking advantage of these instances - directly through the par
and race
functions from these classes, or by using the Parallel
newtype wrapper that enables parallel behaviours through the Applicative
and Alternative
operators.
In the following example, using the newtype, two Ajax requests are initiated simultaneously (rather than in sequence, as they would be for Aff
):
runParallel (f <$> parallel (Ajax.get "http://foo.com") <*> parallel (Ajax.get "http://foo.com"))
And the equivalent using the MonadPar
function directly:
par f (Ajax.get "http://foo.com") (Ajax.get "http://foo.com")
The race
function from MonadPar
or the (<|>)
operator of the Alt
instance of Parallel
allows you to race two asynchronous computations, and use whichever value comes back first (or the first error, if both err).
The runParallel
function allows you to unwrap the Aff
and return to normal monadic (sequential) composition.
A parallel computation can be canceled if both of its individual components can be canceled.
API Docs
API documentation is published on Pursuit.
See also
A good overview of Aff was provided during LambdaConf 2015 conference