2

I'm trying to figure out a good way to implement a request response pattern, where a monad can request the monad runner to perform an action and return a value back to the monad.

The reason I want to do this is because I have a bunch of tasks to perform where some of the work is IO based and some is CPU based. I want a bunch of cpu threads to do the cpu work, hand off io tasks to another thread designated to do disk work, and be then free to work on other CPU tasks while the disk thread finds a value for them. A task might be something like:

do some cpu work 
request load a value from disk
do some more cpu work  
request another value from disk
... etc ..

I created the following as a simple way to do this, where ReqRes, below, represent the disk based tasks. However, in testIO, it has a waterfall look to it where the code marches off to the right, every time it makes a new request, due to nested functions.

I was wondering if there is a cleaner way to do it, that doesn't require this nested function structure.

module ReqResPattern where

import Control.Monad.IO.Class (MonadIO(..))

data ReqRes m = RR1 String (String -> m (ReqRes m)) | RR2 Int (Int -> m (ReqRes m)) | Fin

testIO :: MonadIO m => m (ReqRes m)
testIO =
  do
    return $ RR1 "fred"
      (\x ->
         do
           liftIO $ putStrLn $ "str: " ++ x
           return $ RR2 1
             (\y ->
                do
                  liftIO $ putStrLn $ "int: " ++ (show y)
                  return $ Fin 
             )
      )


runTestIO :: IO ()
runTestIO =
  doit testIO
  where
    doit :: IO (ReqRes IO) -> IO ()
    doit m = 
      do
        v <- m
        case v of
          RR1 v f -> doit $ f (v ++ " foo") 
          RR2 v f -> doit $ f (v+1)
          Fin -> return ()
        return ()
redfish64
  • 551
  • 3
  • 10
  • You are talking about **threads** but I don't see any threads in your code. What about really spawning some with [`forkIO`](https://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Concurrent.html#v:forkIO) and communicating via [`Chan`](https://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Concurrent-Chan.html)? In this case you will have chan writes instead of nested functions. – max taldykin Jul 26 '18 at 11:52
  • I didn't want to complicate the example, but runTestIO would assign which threads would work on the result in the real situation. I mentioned threads because I was trying to fill in the background as to to why I wanted to do this. In the real code, I will be using Chan writes, but I don't want the cpu threads to pause. That's why I want to create these snippets of monad computations, so that I can split them up and assign them to different threads. If it would help, I can add actual threads and a chan to communicate to the example. – redfish64 Jul 26 '18 at 12:53
  • 1
    I don't have time to provide a full answer now, but the pattern of "request a value from outside the context before proceeding inside it" is one way of looking at what free monads do. – Carl Jul 26 '18 at 14:20
  • 1
    Can't you just use normal IO ? (and evaluate them until you need them). What I mean is, `(x, io)` to do later `io x` is actually equivalent to `io x`, even if it's an IO. Haskell is lazy and `io x` will only get evaluated when you try to extract the value of it. – mb14 Jul 26 '18 at 15:31
  • Is your question about how to make the code you posted more readable, or is it about whether your scheme re. using threads to avoid blocking makes sense? – jberryman Jul 26 '18 at 15:39
  • @jberryman it's about making it more readable. – redfish64 Jul 27 '18 at 06:24

1 Answers1

0

I created a monad transformer specifically to do this. Unless someone can show me that it's easily done another way, and is just clutter, I'll probably create a haskell package for this.

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module ReqResPattern where

import Control.Monad.IO.Class (MonadIO(..))
import Data.Fix (Fix(..))
import Control.Monad.Fix
import Debug.Trace(trace)

-- | This is a monad transformer that contains a simple category that tells what
--   type of operation it is. Then when run, the monad will stop everytime the category
--   changes. A specific example of use would be if you wanted to run some code within
--   a thread pool for cpu tasks, another for disk tasks, and a final thread pool for
--   network tasks.
--
--   You could then easily designate which work to do in which thread
--   by using "switchCat" and then feeding the monad to the appropriate thread pool using
--   an MVar or something.

data CatT catType m a = CatT { runCatT :: (m (Either (CatT catType m a) a)),
                               cat :: Maybe catType
                               -- ^ This is the category that the monad starts in.
                               -- It may switch categories at any time by returning
                               -- a new CatT.
                             }

instance Functor m => Functor (CatT cat m) where
  fmap f (CatT a cat) = CatT (fmap (cattfmap f) a) cat

cattfmap :: Functor m => (a -> b) -> (Either (CatT cat m a) a) -> (Either (CatT cat m b) b)
cattfmap f (Left ct) = Left $ fmap f ct
cattfmap f (Right a) = Right $ f a

instance Monad m => Applicative (CatT cat m) where
  pure x = CatT (pure (Right x)) Nothing
  (<*>) = cattapp

cattapp :: forall m a b cat . Monad m => CatT cat m (a -> b) -> CatT cat m a -> CatT cat m b
cattapp cmf@(CatT mf cat1) cma@(CatT ma cat2) = CatT (ma >>= mappedMf mf) cat2
  --the type is cat2 because this is the type the resulting structure will start with
  where
    mappedMf :: m (Either (CatT cat m (a -> b)) (a -> b)) -> Either (CatT cat m a) a -> m (Either (CatT cat m b) b)
    mappedMf mf ea = fmap (doit ea) mf

    doit :: Either (CatT cat m a) a -> Either (CatT cat m (a -> b)) (a -> b) -> (Either (CatT cat m b) b)
    doit (Left ca) (Left cf) = Left $ cf <*> ca
    doit (Right a) (Left cf) = Left $ cf <*> (pure a)
    doit (Left ca) (Right f) = Left $ (pure f) <*> ca
    doit (Right a) (Right f) = Right $ f a

instance (Eq cat, Monad m) => Monad (CatT cat m) where
  (>>=) = cattglue

cattglue :: forall m a b cat . (Monad m, Eq cat) => (CatT cat m a) -> (a -> (CatT cat m b)) -> (CatT cat m b)
cattglue (CatT ma cat1) cfmb = CatT (doit ma cfmb) cat1
  where
    doit :: m (Either (CatT cat m a) a) -> (a -> (CatT cat m b)) -> m (Either (CatT cat m b) b)
    doit ma famb = ma >>= (flip doit2 famb)
    doit2 :: (Either (CatT cat m a) a) -> (a -> (CatT cat m b)) -> m (Either (CatT cat m b) b)
    --if we are already calling another cat, we just glue that one and use it as the inner cat
    doit2 (Left ca) f = return $ Left $ (ca >>= f)
    --otherwise we are returning an object directly
    doit2 (Right a) f =
      --in this case we have a value, so we pass it to the function to extract
      --the next cat, then run them until we get a cat with a conflicting category
      runCatsUntilIncompatible cat1 (f a)

    runCatsUntilIncompatible :: Maybe cat -> CatT cat m b -> m (Either (CatT cat m b) b)
    runCatsUntilIncompatible cat1 cm2 =
        case (cat1, (cat cm2)) of
          (Nothing, Nothing) -> runCatT cm2
          (Nothing, Just _) -> return $ Left cm2
          (Just a, Just b) | a == b -> runCatT cm2
          (Just _, Nothing) -> (runCatT cm2) >>=
            (\cm2v ->
               case cm2v of
                 (Right v) -> return (Right v)
                 (Left cm3) -> runCatsUntilIncompatible cat1 cm3
            )

          _ -> return $ Left cm2

isCompatibleCats :: Eq ct => (Maybe ct) -> (Maybe ct) -> Bool
isCompatibleCats Nothing _ = False
isCompatibleCats _ Nothing = True
isCompatibleCats (Just a) (Just b) = a == b

switchCat :: (Eq cat, Monad m) => cat -> CatT cat m ()
switchCat c = CatT (return $ Right ()) $ Just c

instance (Eq cat, MonadIO m) => MonadIO (CatT cat m) where
  liftIO io = CatT (fmap Right $ liftIO io) Nothing

data MyCat = DiskCat | CPUCat
  deriving (Eq, Show)

type IOCat cat a = CatT cat IO a

test1 :: IOCat MyCat Int
test1 = do
  liftIO $ putStrLn "A simple cat"
  return 1


test2 :: IOCat MyCat ()
test2 = do
  switchCat CPUCat
  liftIO $ putStrLn "CPU Cat 1"
  switchCat CPUCat
  liftIO $ putStrLn "CPU Cat 2"
  return ()

test2' :: IOCat MyCat ()
test2' = 
  switchCat CPUCat >>
  (liftIO $ putStrLn "CPU Cat 1") >>
  switchCat CPUCat >>
  (liftIO $ putStrLn "CPU Cat 2") >>
  return ()


test2'' :: IOCat MyCat ()
test2'' = 
  switchCat CPUCat >>
  ((liftIO $ putStrLn "CPU Cat 1") >>
   (switchCat CPUCat >>
    ((liftIO $ putStrLn "CPU Cat 2") >>
     return ())))


test3 :: IOCat MyCat ()
test3 = do
  switchCat CPUCat
  liftIO $ putStrLn "CPU Cat 1"
  switchCat DiskCat
  liftIO $ putStrLn "Disk Cat 2"
  switchCat CPUCat
  liftIO $ putStrLn "CPU Cat 3"
  return ()

test3' :: IOCat MyCat ()
test3' = 
  switchCat CPUCat >>
  (liftIO $ putStrLn "CPU Cat 1") >>
  switchCat DiskCat >>
  (liftIO $ putStrLn "Disk Cat 2") >>
  switchCat CPUCat >>
  (liftIO $ putStrLn "CPU Cat 3") >>
  return ()

test3'' :: IOCat MyCat ()
test3'' = 
  switchCat CPUCat >> 
  ((liftIO $ putStrLn "CPU Cat 1") >>
    (switchCat DiskCat >>
     ((liftIO $ putStrLn "Disk Cat 2") >>
      (switchCat CPUCat >>
       ((liftIO $ putStrLn "CPU Cat 3") >>
        return ())))))
redfish64
  • 551
  • 3
  • 10