March 2017
“Lazy Functional State Threads”, Launchbury and Peyton Jones (1993).
Some algorithms and data-structures depend critically on destructive updates for efficiency e.g.:
The IO
and ST
monads allow us to express such algorithms in Haskell with the same complexity as an imperative language.
data IORef a -- from Data.IORef
-- a reference to a value of type `a'
newIORef :: a -> IO (IORef a) -- create
readIORef :: IORef a -> IO a -- read
writeIORef :: IORef a -> a -> IO () -- write
import Data.IORef
main :: IO ()
main = do r <- newIORef 0
incr r
incr r
v <- readIORef r
print v -- ouputs 2
incr :: IORef Int -> IO ()
incr r = do v <- readIORef r
writeIORef r (v+1)
incr :: IORef Int -> IO ()
incr r = do v <- readIORef r
writeIORef r (v+1)
-- is equivalent to:
incr :: IORef Int -> IO ()
incr r = modifyIORef r (+1)
import Data.IORef
import Control.Monad (forM_)
-- sum all values in a list
-- using a mutable reference
sumIO :: Num a => [a] -> IO a
sumIO xs = do
r <- newIORef 0
forM_ xs $
\x -> modifyIORef r (+x)
readIORef r
-- from Data.Array.IO
data IOArray i e
-- indices `i' and elements `e'
-- NB: simplified types (no type classes)
newArray :: (i,i) -> e -> IO (IOArray i e)
newListArray :: (i,i) -> [e] -> IO (IOArray i e)
readArray :: IOArray i e -> i -> IO e
writeArray :: IOArray i e -> i -> e -> IO ()
shuffle :: [a] -> IO [a]
shuffle xs = do
let n = length xs
ar <- makeArray n xs
forM [1..n] $ \i -> do
j <- randomRIO (i,n)
vi <- readArray ar i
vj <- readArray ar j
writeArray ar j vi
return vj
where
makeArray :: Int -> [a] -> IO (IOArray Int a)
makeArray n xs = newListArray (1,n) xs
IO
monad “taints” the types of every function using IORefsrunIO :: IO a -> a
because it would allow arbitrary input/output interleaved with pure computationsST
(state threads)
ST s a
is a computation using mutable references that yields a value of type a
.
The type parameter s
is a phantom:
data STRef s a -- from Data.STRef
-- mutable reference to a value `a'
newSTRef :: a -> ST s (STRef s a)
readSTRef :: STRef s a -> ST s a
writeSTRef :: STRef s a -> a -> ST s ()
modifySTRef :: STRef s a -> (a -> a) -> ST s ()
example :: ST s Int
example = do r <- newSTRef 0
incr r
incr r
readSTRef r
incr :: STRef s Int -> ST s ()
incr r = modifySTRef r (+1)
We use runST
to run ST computations, e.g.:
The result is just an integer.
The type of runST
is almost like:
Runs an ST computation and yields a (pure) value a
(more later).
sum' :: Num a => [a] -> a
sum' xs = runST $ do
r <- newSTRef 0
forM_ xs $
\x -> modifySTRef r (+x)
readSTRef r
Note that sum'
is a pure function — it has exactly the same type as the Prelude sum
.
What happens if we use a reference from one ST computation in another?
a
is evaluated before b
, the result would be 1b
is evaluated before a
, the result would be 2This is not good: we loose referencial transparency!
The real type of runST
prevents the above:
runST
does not admit a type that “leaks” references:
Depth-first search (DFS) of a graph:
dfs g v =
runST $ do marks <- newArray (bounds g) False
dfs' g [v] marks
dfs' g [] marks = return []
dfs' g (n:ns) marks = do
vis <- readArray marks n
if vis then
dfs' g ns marks
else do
writeArray marks n True
ns' <- dfs' g ((g!n)++ns) marks
return (n:ns')
dfs' :: Graph -> [Vertex]
-> STArray s Vertex Bool -> ST s [Vertex]
dfs :: Graph -> Vertex -> [Vertex]
The result of the auxiliary dfs'
is a monadic computation but result of the wrapper dfs
is just a list of vertices
Hence:
dfs
can freely combined with other Haskell functions;(Examples in the following slides.)
sum' :: Num a => [a] -> a
sum' xs = runST $ do
r <- newSTRef 0
forM_ xs $ \x -> modifySTRef r (+x)
readSTRef r
modifySTRef
does not force evaluation of the function(+x)
readSTRef
only at the endmodifySTRef'
instead of modifySTRef