Before showing what you the equivalent version of our program using Aff
, let's first overview some of its concepts, so that it's easier to understand the upcoming code.
Aff
as an effect monad must support the following features to be truly asynchronous:
- handles errors that may arise during its computation
- returns some computation's output
- can be cancelled if it's no longer needed
To model both errors and output, we can use either Maybe a
or Either a b
. Since the error might be important for some parties, Maybe a
can't be used. Rather, we'll use Either Error outputType
.
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 no longer needed. Since our present interests do not require cancellation, we can use a no-op Canceler: nonCanceler
For our purposes, we need an Aff
to run inside of an Effect
monad context. In other words, we need some function that takes an Aff
and returns an Effect a
. If one looks through Aff
's docs, the only one that does this is runAff_
:
runAff_ :: forall a. (Either Error a -> Effect Unit) -> Aff a -> Effect Unit
Breaking this down, runAff_
takes two arguments (explained in reverse):
- an
Aff
value to run in theEffect
monad context - a function for handling the asynchronous computation that failed with an
Error
value or a successful output value,a
.
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
Next, we need to create an Aff
value, hence a function that returns an Aff a
. 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 `runAff_` requires
(i.e. `(Either Error a -> Effect Unit)`),
output an `Aff a`
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 runAffFunction = -- implementation
Since the implementation will need to return an Effect Canceler
, we can do one of two things:
- Lift a canceller into
Effect
viapure
. This is pointless because then ourAff
wouldn't do anything. - Create an
Effect a
and use Functor's dervied function,voidLeft
($>
), withnonCanceler
-- for a refresher on voidLeft
(Box 1) voidLeft "2" == (Box 1) $> 2 == (Box 2)
-- alias is: "$>"
voidLeft :: forall f a b. Functor f => f a -> b -> f b
voidLeft box b = (\_ -> 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 = (effectBox runAff_RequiredFunction) $> nonCanceler
effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
effectBox raRF = -- 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 = -- implementation
The only place we could insert raF
is in (String -> Effect Unit)
. Thus, we come up with this function:
effectBox :: (Either Error String -> Effect Unit) -> Effect Unit
effectBox raRF =
question message (\userInput -> raRF (Right userInput)) interface
-- (raRF <<< 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 runAffFunction = (effectBox runAffFunction) $> nonCanceler
effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
effectBox raRF = question message (raRF <<< Right) interface
Cleaning it up and including the arguments, we get:
affQuestion :: String -> Interface -> Aff String
affQuestion mesage interface = makeAff go
where
go :: (Either Error a -> Effect Unit) -> Effect Canceler
go raRF = question message (raRF <<< Right) interface