Disclaimer: this is an old blog post from a very old wordpress blog and may contain inacuracies. I reproduced it as is for sentimental reasons. I may revisit this theme later.
I have a difficulty to understand functional programming concepts that I can’t put to some very simple and natural use (natural for me, of course). I need to find the perfect simple example to implement to finally understand something. And I’m not a computer scientist, so things like parsers and compilers have very little appeal to me (probably because I don’t understand them…). I’m a physicist, so this drives me to look for physical problems that can be implemented in Haskell so I can understand some concepts.
Monad transformers still eludes me. But I think I finally got the perfect subject were I can understand them: stochastic processes! First some book keeping:
import Control.Monad.State
import Control.Monad
import Control.Monad.Rand
Now, stochastic processes have characteristics related to two different monads. In one hand, they are dynamical processes, and the way to implement dynamics in Haskell is with state monads. For example, if I want to iterate the logistic map:
$$x_{t+1} = \alpha x_t\left(1-x_t\right)$$
$$ teste = teste $$
I could do the following:
f :: Double -> Double
f x = 4*x*(1-x)
logistic :: State Double Double
logistic = do x0 <- get
let x1 = f x
put x1
return x1
runLogistic :: State Double [Double]
runLogistic n x0= evalState (replicateM n logistic) x0
Running this on ghci would give you, for example:
*Main> runLogistic 5 0.2
[0.6400000000000001,0.9215999999999999,0.28901376000000045, 0.8219392261226504,0.5854205387341]
So we can make the loose correspondence: dynamical system ↔ state monad.
On the other hand, stochastic processes are compositions of random variables, and this is done with the Rand monad (found in Control.Monad.Random
). As an example, the Box-Muller formula tells us that, if I have two inpendent random variables $x$ and $y$, distributed uniformly between in the \([0, 1]\) interval, then, the expression:
$$\sqrt{-2\log(x)}\cos(2\pi y)$$
will be normally distributed. We can write then:
boxmuller :: Double -> Double -> Double
boxmuller x y = sqrt(-2*log x)*cos(2*pi*y)
normal :: Rand StdGen Double -- normally distributed
normal = do x <- getRandom
y <- getRandom
return $ boxmuller x y
normals n = replicateM n normal -- n independent samples from normal
Running this function we get what we need:
*Main> (evalRand $ normals 5) (mkStdGen 0) =
[0.1600255836730147,0.1575360140445035,-1.595627933129274,
-0.18196791439834512,-1.082222285056746]
So what is a stochastic process? In very rough terms: is a dynamical system with random variables. So we need a way to make the Rand
monad to talk nicely with the State
monad. The way to do this is to use a monad transformer, in this case, the StateT
transformer. Monad transformers allows you to combine the functionalities of two different monads. In the case of the StateT
monads, they allow you to add a state to any other monad you want. In our case, we want to wrap the Rand
monad inside a StateT
transformer and work with things of type:
foo :: StateT s (Rand StdGen) r
This type represent a monad that can store a state with type s, like the state monad, and can generate random variables of type r, like the rand monad. In general we would have a type
foo2 ::(MonadTrans t, Monad m) => t m a
In this case, t = StateT s
and m = Rand StdGen
. The class MonadTrans
is defined in Control.Monad.Trans
, and provides the function:
lift :: (MonadTrans t, Monad m) => m a -> t m a
In this case, t
is itself a monad, and can be treated like one through the code. It works like this: inside a do expression you can use the lift
function to access the inner monad. Things called with lift will operate in the inner monad. Things called without lift
will operate in the outer monad.
So, suppose we want to simulate this very simple process:
$$x_{t+1} = x_t + \eta_t$$
where \(\eta_t\) is drawn from a normal distribution. We would do:
randomWalk :: StateT Double (Rand StdGen) Double
randomWalk = do eta <- lift normal
x <- get
let x' = x + eta
put x'
return x'
runWalk :: Int -> Double -> StdGen -> [Double]
runWalk n x0 gen = evalRand (replicateM n $ evalStateT randomWalk x0) gen
The evalStateT
function is just evalState adapted to run a StateT monad. Running this on ghci we get:
*Main> runWalk 5 0.0 gen
[0.1600255836730147,0.1575360140445035,-1.595627933129274,
-0.18196791439834512,-1.082222285056746]
This is what we can accomplish: we can easily operate simultaneously with functions that expect a state monad, like put and get, we can unwrap things with <-
from the inner Rand
monad by using lift
, and we can return things to the state monad. We could have any monad inside the StateT
transformer. For example, we could have another State
monad. Here is a fancy implementation of the Fibonacci sequence using a State
monad (that stores the last but one value in the sequence as its internal state) inside a StateT
transfomer (that stores the last value of the sequence):
fancyFib :: StateT Int (State Int) Int
fancyFib = do old <- lift get
new <- get
let new' = new + old
old' = new
lift $ put old'
put new'
return new
fancyFibs :: Int -> StateT Int (State Int) [Int]
fancyFibs n = replicateM n fancyFibs
And we can run this to get:
*Main> evalState (evalStateT (fancyFibs 10) 1) 0
[1,1,2,3,5,8,13,21,34,55]