Skip to content

Commit b79371c

Browse files
authored
Merge pull request #106 from natefaubion/free-impl
4.0.0 rewrite
2 parents 32f5d08 + 69bf7ed commit b79371c

15 files changed

+2332
-1282
lines changed

README.md

Lines changed: 170 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@
33
[![Latest release](http://img.shields.io/github/release/slamdata/purescript-aff.svg)](https://github.com/slamdata/purescript-aff/releases)
44
[![Build status](https://travis-ci.org/slamdata/purescript-aff.svg?branch=master)](https://travis-ci.org/slamdata/purescript-aff)
55

6-
An asynchronous effect monad for PureScript.
7-
8-
The moral equivalent of `ErrorT (ContT Unit (Eff e)) a`, for effects `e`.
9-
10-
`Aff` lets you say goodbye to monad transformers and callback hell!
6+
An asynchronous effect monad and threading model for PureScript.
117

128
# Example
139

1410
```purescript
1511
main = launchAff do
1612
response <- Ajax.get "http://foo.bar"
17-
liftEff $ log response.body
13+
log response.body
1814
```
1915

20-
See the [tests](https://github.com/slamdata/purescript-aff/blob/master/test/Test/Main.purs) for more examples.
21-
2216
# Getting Started
2317

2418
## Installation
@@ -38,125 +32,154 @@ deleteBlankLines path = do
3832
saveFile path contents'
3933
```
4034

41-
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.
35+
This looks like ordinary, synchronous, imperative code, but actually operates
36+
asynchronously without any callbacks. Error handling is baked in so you only
37+
deal with it when you want to.
4238

43-
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.
39+
The library contains instances for `Semigroup`, `Monoid`, `Apply`,
40+
`Applicative`, `Bind`, `Monad`, `Alt`, `Plus`, `MonadEff`, `MonadError`, and
41+
`Parallel`. These instances allow you to compose asynchronous code as easily
42+
as `Eff`, as well as interop with existing `Eff` code.
4443

4544
## Escaping Callback Hell
4645

47-
Hopefully, you're using libraries that already use the `Aff` type, so you don't even have to think about callbacks!
46+
Hopefully, you're using libraries that already use the `Aff` type, so you
47+
don't even have to think about callbacks!
4848

49-
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:
49+
If you're building your own library, then you can make an `Aff` from
50+
low-level `Eff` callbacks with `makeAff`.
5051

5152
```purescript
52-
makeAff :: forall e a. ((Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit) -> Aff e a
53+
makeAff :: forall eff a. ((Either Error a -> Eff eff Unit) -> Eff eff (Canceler eff)) -> Aff eff a
5354
```
5455

55-
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.
56+
This function expects you to provide a handler, which should call the
57+
supplied callback with the result of the asynchronous computation.
58+
59+
You should also return `Canceler`, which is just a cleanup effect. Since
60+
`Aff` threads may be killed, all asynchronous operations should provide a
61+
mechanism for unscheduling it.
5662

57-
For example, let's say we have an AJAX request function that expects a callback:
63+
`Control.Monad.Aff.Compat` provides functions for easily binding FFI
64+
definitions:
5865

5966
```javascript
60-
exports.ajaxGet = function(callback) { // accepts a callback
61-
return function(request) { // and a request
62-
return function() { // returns an effect
63-
doNativeRequest(request, function(response) {
64-
callback(response)(); // callback itself returns an effect
65-
});
66-
}
67-
}
68-
}
67+
exports._ajaxGet = function (request) { // accepts a request
68+
return function (onError, onSuccess) { // and callbacks
69+
var req = doNativeRequest(request, function (err, response) { // make the request
70+
if (err != null) {
71+
onError(err); // invoke the error callback in case of an error
72+
} else {
73+
onSuccess(response); // invoke the success callback with the reponse
74+
}
75+
});
76+
77+
// Return a canceler, which is just another Aff effect.
78+
return function (cancelError) {
79+
return function (cancelerError, cancelerSuccess) {
80+
req.cancel(); // cancel the request
81+
cancelerSuccess(); // invoke the success callback for the canceler
82+
};
83+
};
84+
};
85+
};
6986
```
7087

7188
```purescript
72-
foreign import ajaxGet :: forall e. (Response -> Eff e Unit) -> Request -> Eff e Unit
89+
foreign import _ajaxGet :: forall eff. Request -> EffFnAff (ajax :: AJAX | eff) Response
7390
```
7491

7592
We can wrap this into an asynchronous computation like so:
7693

7794
```purescript
78-
ajaxGet' :: forall e. Request -> Aff e Response
79-
ajaxGet' req = makeAff (\error success -> ajaxGet success req)
95+
ajaxGet :: forall eff. Request -> Aff (ajax :: AJAX | eff) Response
96+
ajaxGet = fromEffFnAff <<< _ajaxGet
8097
```
8198

82-
This eliminates callback hell and allows us to write code simply using `do` notation:
99+
This eliminates callback hell and allows us to write code simply using `do`
100+
notation:
83101

84102
```purescript
85-
do response <- ajaxGet' req
86-
liftEff $ log response.body
103+
example = do
104+
response <- ajaxGet req
105+
log response.body
87106
```
88107

89108
## Eff
90109

91-
All purely synchronous computations (`Eff`) can be lifted to asynchronous computations with `liftEff` defined in `Control.Monad.Eff.Class` (see [here](https://github.com/purescript/purescript-eff)).
110+
All purely synchronous computations (`Eff`) can be lifted to asynchronous
111+
computations with `liftEff` defined in `Control.Monad.Eff.Class`.
92112

93113
```purescript
94-
import Control.Monad.Eff.Class
95-
96114
liftEff $ log "Hello world!"
97115
```
98116

99-
This lets you write your whole program in `Aff`, and still call out to synchronous code.
100-
101-
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`:
117+
This lets you write your whole program in `Aff`, and still call out to
118+
synchronous code.
102119

103-
```purescript
104-
do e <- liftEff' myExcFunc
105-
liftEff $ either (const $ log "Oh noes!") (const $ log "Yays!") e
106-
```
120+
If your `Eff` code throws exceptions (`exception :: EXCEPTION`), you can
121+
remove the exception label using `liftEff'`. Exceptions are part of `Aff`s
122+
built-in semantics, so they will always be caught and propagated anyway.
107123

108124
## Dealing with Failure
109125

110-
The `Aff` monad has error handling baked in, so ordinarily you don't have to worry about it.
126+
`Aff` has error handling baked in, so ordinarily you don't have to worry
127+
about it.
111128

112-
When you need to deal with failure, you have several options.
129+
When you need to deal with failure, you have a few options.
113130

114-
1. **Attempt**
115-
2. **Alt**
116-
3. **MonadError**
131+
1. **Alt**
132+
2. **MonadError**
133+
3. **Bracketing**
117134

118-
#### 1. Attempt
135+
#### 1. Alt
119136

120-
If you want to attempt a computation but recover from failure, you can use the `attempt` function:
137+
Because `Aff` has an `Alt` instance, you may also use the operator `<|>` to
138+
provide an alternative computation in the event of failure:
121139

122140
```purescript
123-
attempt :: forall e a. Aff e a -> Aff e (Either Error a)
141+
example = do
142+
result <- Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
143+
pure result
124144
```
125145

126-
This returns an `Either Error a` that you can use to recover from failure.
146+
#### 2. MonadError
127147

128-
```purescript
129-
do e <- attempt $ Ajax.get "http://foo.com"
130-
liftEff $ either (const $ log "Oh noes!") (const $ log "Yays!") e
131-
```
148+
`Aff` has a `MonadError` instance, which comes with two functions:
149+
`catchError`, and `throwError`.
132150

133-
#### 2. Alt
134-
135-
Because `Aff` has an `Alt` instance, you may also use the operator `<|>` to provide an alternative computation in the event of failure:
151+
These are defined in
152+
[purescript-transformers](http://github.com/purescript/purescript-transformers).
153+
Here's an example of how you can use them:
136154

137155
```purescript
138-
do result <- Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
139-
return result
156+
example = do
157+
resp <- Ajax.get "http://foo.com" `catchError` \_ -> pure defaultResponse
158+
when (resp.statusCode /= 200) do
159+
throwError myErr
160+
pure resp.body
140161
```
141162

142-
#### 3. MonadError
163+
#### 3. Bracketing
143164

144-
`Aff` has a `MonadError` instance, which comes with two functions: `catchError`, and `throwError`.
145-
146-
These are defined in [purescript-transformers](http://github.com/purescript/purescript-transformers).
147-
Here's an example of how you can use them:
165+
`Aff` threads can be cancelled, but sometimes we need to guarantee an action
166+
gets run even in the presence of exceptions or cancellation. Use `bracket` to
167+
acquire resources and clean them up.
148168

149169
```purescript
150-
do resp <- (Ajax.get "http://foo.com") `catchError` (const $ pure defaultResponse)
151-
if resp.statusCode != 200 then throwError myErr
152-
else pure resp.body
170+
example =
171+
bracket
172+
(openFile myFile)
173+
(\file -> closeFile file)
174+
(\file -> appendFile "hello" file)
153175
```
154176

155-
Thrown exceptions are propagated on the error channel, and can be recovered from using `attempt` or `catchError`.
177+
In this case, `closeFile` will always be called regardless of exceptions once
178+
`openFile` completes.
156179

157180
## Forking
158181

159-
Using the `forkAff`, you can "fork" an asynchronous computation, which means
182+
Using `forkAff`, you can "fork" an asynchronous computation, which means
160183
that its activities will not block the current thread of execution:
161184

162185
```purescript
@@ -167,66 +190,111 @@ Because Javascript is single-threaded, forking does not actually cause the
167190
computation to be run in a separate thread. Forking just allows the subsequent
168191
actions to execute without waiting for the forked computation to complete.
169192

170-
If the asynchronous computation supports it, you can "kill" a forked computation
171-
using the returned canceler:
193+
Forking returns a `Fiber eff a`, representing the deferred computation. You can
194+
kill a `Fiber` with `killFiber`, which will run any cancelers and cleanup, and
195+
you can observe a `Fiber`'s final value with `joinFiber`. If a `Fiber` threw
196+
an exception, it will be rethrown upon joining.
172197

173198
```purescript
174-
canceler <- forkAff myAff
175-
canceled <- canceler `cancel` (error "Just had to cancel")
176-
_ <- liftEff $ if canceled then (log "Canceled") else (log "Not Canceled")
199+
example = do
200+
fiber <- forkAff myAff
201+
killFiber (error "Just had to cancel") fiber
202+
result <- try (joinFiber fiber)
203+
if isLeft result
204+
then (log "Canceled")
205+
else (log "Not Canceled")
177206
```
178207

179-
If you want to run a custom canceler if some other asynchronous computation is
180-
cancelled, you can use the `cancelWith` combinator:
208+
209+
## AVars
210+
211+
The `Control.Monad.Aff.AVar` module contains asynchronous variables, which
212+
are very similar to Haskell's `MVar`.
213+
214+
`AVar`s represent a value that is either full or empty. Calling `takeVar` on
215+
an empty `AVar` will queue until it is filled by a matching `putVar`.
181216

182217
```purescript
183-
otherAff `cancelWith` myCanceler
218+
example = do
219+
var <- makeEmptyVar
220+
_ <- forkAff do
221+
value <- takeVar var
222+
log $ "Got a value: " <> value
223+
_ <- forkAff do
224+
delay (Milliseconds 100.0)
225+
putVar var "hello"
226+
pure unit
227+
```
228+
```
229+
(Waits 100ms)
230+
> Got a value: hello
184231
```
185232

186-
## AVars
187-
188-
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.
233+
Likewise, calling `putVar` will queue until it is taken:
189234

190235
```purescript
191-
do v <- makeVar
192-
_ <- forkAff do
193-
delay (Milliseconds 50.0)
194-
putVar v 1.0
195-
a <- takeVar v
196-
liftEff $ log ("Succeeded with " ++ show a)
236+
example = do
237+
var <- makeEmptyVar
238+
_ <- forkAff do
239+
delay (Milliseconds 100.0)
240+
value <- takeVar var
241+
log $ "Got a value: " <> value
242+
putVar var "hello"
243+
log "Value taken"
244+
```
245+
```
246+
(Waits 100ms)
247+
> Value taken
248+
> Got a value: hello
197249
```
198250

199-
You can use these constructs as one-sided blocking queues, which suspend (if
200-
necessary) on `take` operations, or as asynchronous, empty-or-full variables.
251+
These combinators (and a few more) can be used as the building blocks for
252+
complex asynchronous coordination.
201253

202254
## Parallel Execution
203255

204-
There are `MonadPar` and `MonadRace` instances defined for `Aff`, allowing for parallel execution of `Aff` computations.
256+
The `Parallel` instance for `Aff` makes writing parallel computations a breeze.
205257

206-
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.
207-
208-
In the following example, using the newtype, two Ajax requests are initiated simultaneously (rather than in sequence, as they would be for `Aff`):
258+
Using `parallel` from `Control.Parallel` will turn a regular `Aff` into
259+
`ParAff`. `ParAff` has an `Applicative` instance which will run effects in
260+
parallel, and an `Alternative` instance which will race effects, returning the
261+
one which completes first (canceling the others). To get an `Aff` back, just
262+
run it with `sequential`.
209263

210264
```purescript
211-
runParallel (f <$> parallel (Ajax.get "http://foo.com") <*> parallel (Ajax.get "http://foo.com"))
265+
-- Make two requests in parallel
266+
example =
267+
sequential $
268+
Tuple <$> parallel (Ajax.get "https://foo.com")
269+
<*> parallel (Ajax.get "https://bar.com")
212270
```
213271

214-
And the equivalent using the `MonadPar` function directly:
215-
216272
```purescript
217-
par f (Ajax.get "http://foo.com") (Ajax.get "http://foo.com")
273+
-- Make a request with a 3 second timeout
274+
example =
275+
sequential $ oneOf
276+
[ parallel (Just <$> Ajax.get "https://foo.com")
277+
, parallel (Nothing <$ delay (Milliseconds 3000.0))
278+
]
218279
```
219280

220-
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).
281+
```purescript
282+
tvShows =
283+
[ "Stargate_SG-1"
284+
, "Battlestar_Galactics"
285+
, "Farscape"
286+
]
221287
222-
The `runParallel` function allows you to unwrap the `Aff` and return to normal monadic (sequential) composition.
288+
getPage page =
289+
Ajax.get $ "https://wikipedia.org/wiki/" <> page
223290
224-
A parallel computation can be canceled if both of its individual components can be canceled.
291+
-- Get all pages in parallel
292+
allPages = parTraverse getPage tvShows
293+
294+
-- Get the page that loads the fastest
295+
fastestPage = parOneOfMap getPage tvShows
296+
```
225297

226298
# API Docs
227299

228300
API documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-aff).
229-
230-
# See also
231-
232-
[A good overview of Aff](https://github.com/degoes-consulting/lambdaconf-2015/blob/master/speakers/jdegoes/async-purescript/presentation.pdf) was provided during LambdaConf 2015 conference

0 commit comments

Comments
 (0)