1
\$\begingroup\$

Whether based on MVar or TVar, async implementation are always based on operation on some underlying monad IO and STM.

Making Async a monad on its own, as in F# async computation builder, if done in a naïve way require littering with unsafePerformIO which does not feel very haskellish.

module Async2(Async, async, wait) where

import           Control.Concurrent (forkIO)
import Control.Monad
import Control.Concurrent.MVar
import System.IO.Unsafe

data Async a = Async (MVar a)

wait :: Async a -> IO a
wait  (Async m) = takeMVar m

-- we can't lift it without unsafeperformIO !
async :: IO a -> Async a
async action = unsafePerformIO $ do
  m <- newEmptyMVar
  forkIO $ do r <- action; putMVar m r
  return $ Async m

-- we can't make it a monad without unsafePerformIO !
instance Monad Async where
  return a = Async $ unsafePerformIO $ newMVar a
  m >>= f = let a = unsafePerformIO $ wait m
            in f a 

I can forego having a monad and rely on nested function composition, but that's not very nice compared to do notation. So instead I can try to add some MonadIO somewhere:

module Async3(Async, async, wait) where

import           Control.Concurrent.MVar
import           Control.Concurrent (forkIO)
import Control.Monad
import Control.Monad.IO.Class

data Async m a =  Async (m (MVar a))

async ::  MonadIO m =>  IO a -> (Async m a)
async action = Async $ liftIO $ do
  m <- newEmptyMVar
  forkIO $ do r <- action; putMVar m r
  return m


wait :: MonadIO m => Async m a -> m a
wait  (Async m) = do  mv <- m
                      liftIO $ readMVar mv


instance MonadIO m  =>  Monad (Async m) where
  return a = Async $ liftIO $ newMVar a
  ma >>= f = Async $ do
   r <- wait ma
   let (Async mv) = f r
   mv

-- automatic def
instance  MonadIO m => Functor (Async m) where
  fmap f a' = a' >>= pure . f

instance  MonadIO m => Applicative (Async m) where
  pure = return
  (<*>)  = ap

instance MonadIO m => MonadIO (Async m) where
  liftIO m = Async ( liftIO undefined)

When parametarized by such MonadIO, I can leverage my IO context to get the monadic do notation.

#!/usr/bin/env stack
-- stack --install-ghc --resolver lts-5.13 runghc --package http-conduit
module MainAsync3 where

import Control.Concurrent(forkIO, threadDelay)
import           Async3(Async, async, wait) 

main :: IO ()
main = do
    c <- wait $ do
           r <- do

                   async $ threadDelay 1000000
                   return "hello"
           s <- do
                   async $ threadDelay 1000000
                   return " world"
           return $ r ++ s

    print c 
    return ()

Except I can't perform IO from within Async m itself, so that motivates another round:

module Async4(Async, async, wait) where

import           Control.Concurrent.MVar
import           Control.Concurrent (forkIO)
import Control.Monad
import Control.Monad.IO.Class

data Async m a =  Async (m (Res a))
data Res a = RMVar (MVar a) | Done a

async ::  MonadIO m =>  IO a -> Async m a
async action = Async $ liftIO $ do
  m <- newEmptyMVar
  forkIO $ do r <- action; putMVar m r
  return $ RMVar m


wait :: MonadIO m => Async m a -> m a
wait  (Async m) = do  r <- m
                      case r of
                        RMVar mv -> liftIO $ readMVar mv
                        Done a -> return a


instance MonadIO m  =>  Monad (Async m) where
  return a = Async $ return $ Done a
  ma >>= f = Async $ do
   r <- wait ma
   let (Async mv) = f r
   mv


-- automatic def
instance  MonadIO m => Functor (Async m) where
  fmap f a' = a' >>= pure . f

instance  MonadIO m => Applicative (Async m) where
  pure = return
  (<*>)  = ap

instance MonadIO m => MonadIO (Async m) where
  liftIO m = Async ( liftIO $ Done <$> m)

Which we can call using:

#!/usr/bin/env stack
-- stack --install-ghc --resolver lts-5.13 runghc
module MainAsync4 where

import Control.Concurrent(forkIO, threadDelay)
import           Async4(Async, async, wait) 
import Control.Monad.IO.Class

main :: IO ()
main = do
    putStrLn "starting"
    c <- wait $ do  -- I can wait an async computation
           r <- do -- I can compose sequentially async
                   liftIO $ putStrLn "computing hello" -- I can do IO within async
                   async $ threadDelay 1000000
                   return "hello"
           s <- do
                   async $ threadDelay 1000000
                   return " world"
           return $ r ++ s 

    print c 
    return ()

I feel like it is quite complicated, though. What approaches are there to regain some sanity and nicely hide those calls so that I can get a monad modulo some IO and write nice imperative looking code while composing async operations?

In the end, I know that IO might launch missiles but when you don't launch missiles there must be ways to declare it somehow.

Are there ways known to be superior to deal with these situations? The same problem arises if we swap IO with STM. It's just that I build a new composable language on top of another.

Would effect handlers provide a better answer for instance?

\$\endgroup\$
1
\$\begingroup\$

Async3

I'm not sure your MainAsync3 works the way you want it to. When I bump the threadDelays values to 5 seconds each the running time increases to 10 seconds suggesting that the async operations are not being done in parallel. And also the same for MainAsync4. Am I missing something?

Btw, a simple way to see wall clock time is to:

import Data.Time
...

main = do
  getCurrentTime >>= print
  ...
  getCurrentTime >>= print

Note that timing functions like timeIt returns execution time which will be near 0 for these examples.

Control.Concurrent.Async

Have a look at the async package. It implements async within the IO monad. AFAICT it does not use unsafePerformIO.

Why a Monad?

If you can perform async operations in IO, why should we create a special monad for it? Why does F# have an Async monad?

Here are some reasons I came up with:

  1. The bind operation (let! in F#?) will automatically wait on computations. On the other hand, normal IO operations have to be lifted.

  2. In Haskell if it's a monad it is also a Functor and Applicative, and then we can use generic applicative operations like fmap, (<*>), etc. That's the point of the promise package - it just wraps the result of an async in a newtype to give you a monad to use.

  3. One primary reason for implementing a computation as a monad is to restrict what it can do. For instance, the ST monad is designed so that the thread state can't escape the computation.

  4. Another reason to implement a monad is allow you to implement a "run" function which controls execution of the computation. For instance, Haxl implements an async monad which can cache results and batch together multiple requests sent to the same service.

  5. All I know about the F# Async monad I learned from this blog post. Perhaps you could give more insight as to what's useful about the F# Async monad.

\$\endgroup\$
  • \$\begingroup\$ you highlighted a critical point in questioning why it should be a monad. 1. the syntactic benefit is very little. in fsharp there is no 'downside' as everything is IO lifted already 2. good to know 34 agreed I should think about that and read about haxl 5. in fsharp the motivation behind async AFAIU is to rely on the threadpool to speed up IO bound computations. we get that for free with IO monad and green thread. also allow to specify, within an async task, that another async task will be created and synchronized (AsyncStartChild), but we also get that in IO. Thank you for the 'step aside' \$\endgroup\$ – nicolas Jun 1 '16 at 7:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.