Basic Aff Functions
In this file, we'll show the second way to run an Aff computation called runAff and how to convert Node.ReadLine's question function (i.e. an Effect-based function that requires a callback) into an Aff-based computation using makeAff.
Aff Overview
Let's first overview some of Aff's concepts, so that the upcoming code is easier to understand. To be a truly asynchronous effect monad, Aff must support the following features:
- handles errors that may arise during its computation
- returns some computation's output
- can be cancelled if it's no longer needed
To model the possibility for a computation to return an error or actual output, we can use Either a b. Handling errors and output implies a function. Aff uses the type signature, Either errorType outputType -> Effect Unit, for that.
Lastly, cancelling implies what to do when the computation is either no longer needed or it has failed (but we aren't using the function just discussed above). As an example, one will use Cancelers to clean up resources (e.g. clearTimeout).
newtype Canceler = Canceler (Error -> Aff Unit)
Since our present interests do not require cancellation, we can use a no-op Canceler: nonCanceler
Understanding runAff
For our purposes, we need an Aff to run inside of an Effect monadic context. If one looks through Aff's docs, the only one that does this besides launchAff and its variants is runAff_:
runAff_ :: forall a.
(Either Error a -> Effect Unit) -> -- arg 1
Aff a -> -- arg 2
Effect Unit -- outputted value
Breaking this down, runAff_ takes two arguments (explained in reverse):
- an
Affcomputation to run - a function for handling a possible asynchronous
Errorif the computation fails or the computation's output,a, if it succeeds.
Using it should look something like:
runAff_ (\either -> case either of
Left error -> log $ show error
Right a -> -- do something with 'a' or run cleanup code
)
affValue
We could make the code somewhat easier by using Data.Either (either)
runAff_ (either
(\error -> log $ show error ) -- Left value
(\a -> {- usage or cleanup -} ) -- Right value
)
affValue
Understanding makeAff
Next, we need to convert question from an Effect-based computation into an Aff-based one. Looking through Pursuit again, makeAff is the only function that does this:
makeAff :: forall a. ((Either Error a -> Effect Unit) -> Effect Canceler) -> Aff a
Breaking this down, makeAff takes only one argument. However, the argument is a bit quirky since it takes a function as its argument. We should read it as...
Given a function
that returns an `Effect Canceler`
by using the function that `runAff_` requires
(i.e. `(Either Error a -> Effect Unit)`),
output an `Aff` computation that produces a value of type `a` when `bind`ed
To create this type signature, we'll write something like this:
affValue :: Aff String
affValue = makeAff go
where
go :: (Either Error a -> Effect Unit) -> Effect Canceler
go runAff_RequiredFunction = -- implementation
Since the implementation will need to return an Effect Canceler, we can do one of two things:
- Lift a canceller into
Effectviapure. This is pointless because then ourAffwouldn't do anything. - Create an
Effect aand use Functor's dervied function,voidRight(<$), withnonCanceler
-- for a refresher on voidRight
2 `voidRight` (Box 1) == 2 <$ (Box 1) == (Box 2)
-- alias is: "<$"
voidRight :: forall f a b. Functor f => b -> f a -> f b
voidRight b box = (\_ -> b) <$> box
-- or ignore the monad's inner 'a' and replace it with 'b'
Updating our code to use these two ideas, we now have:
affValue :: Aff String
affValue = makeAff go
where
go :: (Either Error a -> Effect Unit) -> Effect Canceler
go runAff_RequiredFunction = nonCanceler <$ (effectBox runAff_RequiredFunction)
effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
effectBox runAffFunction = -- implementation
We want to use question to print something to the console, get the user's input, and return that value. It's type signature is:
question :: String -> (String -> Effect Unit) -> Interface -> Effect String
question message handleUserInput interface = -- Node binding implementation
The only place we could insert runAffFunction is in (String -> Effect Unit). Thus, we come up with this function:
effectBox :: (Either Error String -> Effect Unit) -> Effect Unit
effectBox runAffFunction =
question message (\userInput -> runAffFunction (Right userInput)) interface
-- (runAffFunction <<< Right) -- less verbose; same thing
Putting it all together and excluding the required arguments, we get:
affValue :: Aff String
affValue = makeAff go
where
go :: (Either Error a -> Effect Unit) -> Effect Canceler
go runAff_RequiredFunction = nonCanceler <$ (effectBox runAff_RequiredFunction)
effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
effectBox runAffFunction = question message (runAffFunction <<< Right) interface
Cleaning it up and including the arguments, we get:
affQuestion :: String -> Interface -> Aff String
affQuestion message interface = makeAff go
where
go :: (Either Error a -> Effect Unit) -> Effect Canceler
go runAffFunction =
nonCanceler <$ question message (runAffFunction <<< Right) interface