Skip to content

4.0.0 rewrite #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Aug 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9fe5a0b
WIP
natefaubion Jun 29, 2017
c7f2c4f
Move things around, add a bunch of tests and instances
natefaubion Jul 3, 2017
1eff561
Remove Either from Sync constructor
natefaubion Jul 3, 2017
51169ff
Revert fancy rows in instances. Simplify liftEff/makeAff wrappers.
natefaubion Jul 3, 2017
b53ec1e
Add AVar bindings
natefaubion Jul 18, 2017
6e23d0a
Use catchError primitive instead of attempt
natefaubion Jul 18, 2017
60fc908
Rework ParAff
natefaubion Jul 20, 2017
d68d117
Add avar dep
natefaubion Jul 22, 2017
f2d67a1
Satisfy linters
natefaubion Jul 22, 2017
7d71cba
Move unnecessary Internal module, cleanup
natefaubion Jul 22, 2017
67ad0d2
Add lazy thread map
natefaubion Jul 23, 2017
04f9696
Applicative instance for Thread
natefaubion Jul 23, 2017
f9be3ef
Thread def cleanup
natefaubion Jul 23, 2017
2c6284e
Stack-safe ParAff
natefaubion Jul 30, 2017
e0caaa4
Synchronous ParAff Alt handling
natefaubion Aug 1, 2017
edce196
Appease the linter
natefaubion Aug 1, 2017
646dd95
Comments, compat, thread -> fiber, generalBracket
natefaubion Aug 5, 2017
5133c20
Suspend, child supervision
natefaubion Aug 8, 2017
1b7a692
Rename bracket condition labels
natefaubion Aug 8, 2017
3391938
Use runFiber in sequential
natefaubion Aug 9, 2017
5320f7e
Add never Aff.
natefaubion Aug 9, 2017
4f04c8a
Provide result to bracket completed
natefaubion Aug 10, 2017
ffc0b6f
Make FFI amenable to purs-bundling
natefaubion Aug 11, 2017
afd150f
FFI cleanup
natefaubion Aug 11, 2017
8b0ce0b
Scheduler for async resumption
natefaubion Aug 12, 2017
8bcb2ae
Linter
natefaubion Aug 12, 2017
93846c9
Refactor internal Fiber API, bug fixes
natefaubion Aug 14, 2017
637e913
Fill in missing functions
natefaubion Aug 15, 2017
116b499
Add supervise combinator
natefaubion Aug 16, 2017
ae7a9f7
Implement supervise with generalBracket
natefaubion Aug 16, 2017
200f2fe
Add launchAff_ and runSuspendedAff
natefaubion Aug 17, 2017
59fd52d
Update README
natefaubion Aug 17, 2017
047eb55
Fix effect typo
natefaubion Aug 18, 2017
08daf3a
Merge branch 'master' into free-impl
natefaubion Aug 18, 2017
7576856
Add a few more tests
natefaubion Aug 18, 2017
69bf7ed
README edits
natefaubion Aug 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 170 additions & 102 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@
[![Latest release](http://img.shields.io/github/release/slamdata/purescript-aff.svg)](https://github.com/slamdata/purescript-aff/releases)
[![Build status](https://travis-ci.org/slamdata/purescript-aff.svg?branch=master)](https://travis-ci.org/slamdata/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!
An asynchronous effect monad and threading model for PureScript.

# Example

```purescript
main = launchAff do
response <- Ajax.get "http://foo.bar"
liftEff $ log response.body
log response.body
```

See the [tests](https://github.com/slamdata/purescript-aff/blob/master/test/Test/Main.purs) for more examples.

# Getting Started

## Installation
Expand All @@ -38,125 +32,154 @@ deleteBlankLines path = do
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.
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.
The library contains instances for `Semigroup`, `Monoid`, `Apply`,
`Applicative`, `Bind`, `Monad`, `Alt`, `Plus`, `MonadEff`, `MonadError`, and
`Parallel`. 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!
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:
If you're building your own library, then you can make an `Aff` from
low-level `Eff` callbacks with `makeAff`.

```purescript
makeAff :: forall e a. ((Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit) -> Aff e a
makeAff :: forall eff a. ((Either Error a -> Eff eff Unit) -> Eff eff (Canceler eff)) -> Aff eff 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.
This function expects you to provide a handler, which should call the
supplied callback with the result of the asynchronous computation.

You should also return `Canceler`, which is just a cleanup effect. Since
`Aff` threads may be killed, all asynchronous operations should provide a
mechanism for unscheduling it.

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

```javascript
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
});
}
}
}
exports._ajaxGet = function (request) { // accepts a request
return function (onError, onSuccess) { // and callbacks
var req = doNativeRequest(request, function (err, response) { // make the request
if (err != null) {
onError(err); // invoke the error callback in case of an error
} else {
onSuccess(response); // invoke the success callback with the reponse
}
});

// Return a canceler, which is just another Aff effect.
return function (cancelError) {
return function (cancelerError, cancelerSuccess) {
req.cancel(); // cancel the request
cancelerSuccess(); // invoke the success callback for the canceler
};
};
};
};
```

```purescript
foreign import ajaxGet :: forall e. (Response -> Eff e Unit) -> Request -> Eff e Unit
foreign import _ajaxGet :: forall eff. Request -> EffFnAff (ajax :: AJAX | eff) Response
```

We can wrap this into an asynchronous computation like so:

```purescript
ajaxGet' :: forall e. Request -> Aff e Response
ajaxGet' req = makeAff (\error success -> ajaxGet success req)
ajaxGet :: forall eff. Request -> Aff (ajax :: AJAX | eff) Response
ajaxGet = fromEffFnAff <<< _ajaxGet
```

This eliminates callback hell and allows us to write code simply using `do` notation:
This eliminates callback hell and allows us to write code simply using `do`
notation:

```purescript
do response <- ajaxGet' req
liftEff $ log response.body
example = do
response <- ajaxGet req
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](https://github.com/purescript/purescript-eff)).
All purely synchronous computations (`Eff`) can be lifted to asynchronous
computations with `liftEff` defined in `Control.Monad.Eff.Class`.

```purescript
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`:
This lets you write your whole program in `Aff`, and still call out to
synchronous code.

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

## Dealing with Failure

The `Aff` monad has error handling baked in, so ordinarily you don't have to worry about it.
`Aff` 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.
When you need to deal with failure, you have a few options.

1. **Attempt**
2. **Alt**
3. **MonadError**
1. **Alt**
2. **MonadError**
3. **Bracketing**

#### 1. Attempt
#### 1. Alt

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

```purescript
attempt :: forall e a. Aff e a -> Aff e (Either Error a)
example = do
result <- Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
pure result
```

This returns an `Either Error a` that you can use to recover from failure.
#### 2. MonadError

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

#### 2. Alt

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

```purescript
do result <- Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
return result
example = do
resp <- Ajax.get "http://foo.com" `catchError` \_ -> pure defaultResponse
when (resp.statusCode /= 200) do
throwError myErr
pure resp.body
```

#### 3. MonadError
#### 3. Bracketing

`Aff` has a `MonadError` instance, which comes with two functions: `catchError`, and `throwError`.

These are defined in [purescript-transformers](http://github.com/purescript/purescript-transformers).
Here's an example of how you can use them:
`Aff` threads can be cancelled, but sometimes we need to guarantee an action
gets run even in the presence of exceptions or cancellation. Use `bracket` to
acquire resources and clean them up.

```purescript
do resp <- (Ajax.get "http://foo.com") `catchError` (const $ pure defaultResponse)
if resp.statusCode != 200 then throwError myErr
else pure resp.body
example =
bracket
(openFile myFile)
(\file -> closeFile file)
(\file -> appendFile "hello" file)
```

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

## Forking

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

```purescript
Expand All @@ -167,66 +190,111 @@ 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:
Forking returns a `Fiber eff a`, representing the deferred computation. You can
kill a `Fiber` with `killFiber`, which will run any cancelers and cleanup, and
you can observe a `Fiber`'s final value with `joinFiber`. If a `Fiber` threw
an exception, it will be rethrown upon joining.

```purescript
canceler <- forkAff myAff
canceled <- canceler `cancel` (error "Just had to cancel")
_ <- liftEff $ if canceled then (log "Canceled") else (log "Not Canceled")
example = do
fiber <- forkAff myAff
killFiber (error "Just had to cancel") fiber
result <- try (joinFiber fiber)
if isLeft result
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:

## AVars

The `Control.Monad.Aff.AVar` module contains asynchronous variables, which
are very similar to Haskell's `MVar`.

`AVar`s represent a value that is either full or empty. Calling `takeVar` on
an empty `AVar` will queue until it is filled by a matching `putVar`.

```purescript
otherAff `cancelWith` myCanceler
example = do
var <- makeEmptyVar
_ <- forkAff do
value <- takeVar var
log $ "Got a value: " <> value
_ <- forkAff do
delay (Milliseconds 100.0)
putVar var "hello"
pure unit
```
```
(Waits 100ms)
> Got a value: hello
```

## 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.
Likewise, calling `putVar` will queue until it is taken:

```purescript
do v <- makeVar
_ <- forkAff do
delay (Milliseconds 50.0)
putVar v 1.0
a <- takeVar v
liftEff $ log ("Succeeded with " ++ show a)
example = do
var <- makeEmptyVar
_ <- forkAff do
delay (Milliseconds 100.0)
value <- takeVar var
log $ "Got a value: " <> value
putVar var "hello"
log "Value taken"
```
```
(Waits 100ms)
> Value taken
> Got a value: hello
```

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

## Parallel Execution

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

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`):
Using `parallel` from `Control.Parallel` will turn a regular `Aff` into
`ParAff`. `ParAff` has an `Applicative` instance which will run effects in
parallel, and an `Alternative` instance which will race effects, returning the
one which completes first (canceling the others). To get an `Aff` back, just
run it with `sequential`.

```purescript
runParallel (f <$> parallel (Ajax.get "http://foo.com") <*> parallel (Ajax.get "http://foo.com"))
-- Make two requests in parallel
example =
sequential $
Tuple <$> parallel (Ajax.get "https://foo.com")
<*> parallel (Ajax.get "https://bar.com")
```

And the equivalent using the `MonadPar` function directly:

```purescript
par f (Ajax.get "http://foo.com") (Ajax.get "http://foo.com")
-- Make a request with a 3 second timeout
example =
sequential $ oneOf
[ parallel (Just <$> Ajax.get "https://foo.com")
, parallel (Nothing <$ delay (Milliseconds 3000.0))
]
```

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).
```purescript
tvShows =
[ "Stargate_SG-1"
, "Battlestar_Galactics"
, "Farscape"
]

The `runParallel` function allows you to unwrap the `Aff` and return to normal monadic (sequential) composition.
getPage page =
Ajax.get $ "https://wikipedia.org/wiki/" <> page

A parallel computation can be canceled if both of its individual components can be canceled.
-- Get all pages in parallel
allPages = parTraverse getPage tvShows

-- Get the page that loads the fastest
fastestPage = parOneOfMap getPage tvShows
```

# API Docs

API documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-aff).

# See also

[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
Loading