Wednesday, May 19, 2010

Haskell & STM, Why no Applicative?

There was a fellow on #haskell the other day who was apprehensive about learning STM. Should he learn it after learning category theory? We assured him that Haskell's STM was fairly simple, whereas category theory is a dense liberal art that you study to enrich your mind. Someone pointed him to the wiki and he went on his way.

Afterwards, I perused the wiki (again). What struck me was that there was no example there that show you how to convert a plain old bunch of IO routines into STM routines. When I went to make one myself, I realized how brain-dead easy it is, but that there's something missing...

Anyway, here's a chalked up example where someone has to perform multiple time-consuming tasks (preferably in parallel), with the interesting routine in bold:


import Control.Applicative
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import Control.Concurrent.STM.TMVar
import Data.DateTime
 
main :: IO ()
main =
    do putStrLn "Without STM"
       t1s <- getCurrentTime
       stuff <- withoutSTM
       t1e <- getCurrentTime
       putStrLn $ show stuff
       putStrLn $ "That took " ++ show (diffSeconds t1e t1s) ++ " seconds"
 

-- this is the routine that actually does stuff
withoutSTM :: IO GroceryStore
withoutSTM =
    do a <- getTomatoesCountFromDB
       b <- haveFreshBerries
       c <- getNameOfCurrentStore
       return $ GroceryStore a b c
 
getTomatoesCountFromDB :: IO Int
getTomatoesCountFromDB =
    do milliSleep 1000  -- simulate slow DB read
       return 5

haveFreshBerries :: IO Bool
haveFreshBerries =
    do milliSleep 1000
       return True

getNameOfCurrentStore :: IO String
getNameOfCurrentStore =
    do milliSleep 1000
       return "Tom's Produce"

data GroceryStore
    = GroceryStore
      { numTomatoes :: Int
      , freshBerriesInStock :: Bool
      , nameOfStore :: String
      }
    deriving (Eq, Show, Ord)
 
-- helpers
milliSleep = threadDelay . (*) 1000
 
 
 
So there you have the plain old imperative code.
 
And here is the same code modified to use STM.
 
 
-- getTomatoesCountFromDB, haveFreshBerries, getNameOfCurrentStore are unchanged from before

-- this is the modified routine
withSTM :: IO GroceryStore
withSTM =
    do a <- stmFork getTomatoesCountFromDB
       b <- stmFork haveFreshBerries
       c <- stmFork getNameOfCurrentStore

       GroceryStore <$> (stmWait a)
                    <*> (stmWait b)
                    <*> (stmWait c)

main :: IO ()
main =
    do putStrLn "With STM"
       t2s <- getCurrentTime
       stuffWithSTM <- withSTM
       t2e <- getCurrentTime
       putStrLn $ show stuffWithSTM
       putStrLn $ "That took " ++ show (diffSeconds t2e t2s) ++ " seconds"
       return ()

-- more helpers
stmWait = atomically

stmFork :: IO a -> IO (STM a)
stmFork m =
    do tmv <- newEmptyTMVarIO
       let m' = m >>= (atomically . putTMVar tmv)
       forkIO m'
       return $ readTMVar tmv
 
 
 
Now the folks who've been playing with this for a while won't find this particularly remarkable, but I wouldn't have used it a year ago when I was first learning Haskell, so I thought someone should make note of it. There are also two important things to take away from this.
 
1. You could only do this if imperative routines are treated as values. The pons asinorum that Eric S. Raymond was referring to is not without its benefits.
 
2. Would an applicative instance for STM be nicer? At the least, we might be able to do code like this instead.
 
       stmWait $ GroceryStore <$> a <*> b <*> c
 

What would be wrong with the naive implementation of this?  e.g.:
 
(<*>) :: (Functor m, Monad m) => m (a -> b) -> m a -> m b
(<*>) mf ma =
    do a <- ma
       f <- mf
       return $ f a
 
 
Again, I'm probably way out of my depth here, so I apologize profusely if this is asinine.

Posted via email from lambdasquirrel's posterous

No comments: