This demonstration was inspired by a Stack Overflow question which asks:
How can I re-assign a variable in a function in Haskell?
For example,
elephant = 0 setElephant x = elephant = x
The short answer is that we don't have a language-level mechanism to model that sort of thing in Haskell, and it's generally not what you want anyway.
Here I give a long answer, demonstrating how to use lenses and the state monad to produce something that looks like a direct translation of the imperative-programming concept of reassigning a variable.
We will be using the (.=)
operator (which also has an alias called assign
)
from the microlens-mtl
library.
(.=) :: MonadState s m => ASetter s s a b -> b -> m ()
The first argument (having type ASetter
) is the lens which specifies what
part of the state to update, and the second is the new value to set at the
position specified by the lens.
The code
{-# LANGUAGE ConstraintKinds, InstanceSigs, PackageImports #-}
module Elephant where
I'm using the PackageImports
extension just for clarity, to show what packages
these imported modules are coming from.
import "base" Control.Monad.IO.Class ( MonadIO (liftIO) )
import "microlens" Lens.Micro ( Lens', lens )
import "microlens-mtl" Lens.Micro.Mtl ( (.=), use )
import "mtl" Control.Monad.State.Class ( MonadState )
import "transformers" Control.Monad.Trans.State ( StateT (runStateT) )
Let's assume that elephants can be modeled by integers.
type Elephant = Integer
We're going to want our program state to have an elephant, so first we'll define what it means to have an elephant. A lens captures this notion nicely.
class HasElephant a
where
elephant :: Lens' a Elephant
We'll say that a monad “has elephant state” if
- it has state, and
- its state has an elephant.
type HasElephantState s m =
(MonadState s m, HasElephant s)
(The HasElephantState
type alias is what we need the ConstraintKinds
extension for.)
Now we can define the action that the question asks for. It operates in some monad that has elephant state, and it assigns a value for the elephant in that state.
setElephant
:: HasElephantState s m
=> Elephant -> m ()
setElephant x =
elephant .= x
Let's also define an action that prints the elephant. In addition to elephant state, this context also requires I/O.
printElephant
:: (HasElephantState s m, MonadIO m)
=> m ()
printElephant =
do
e <- use elephant
liftIO (putStrLn ("The current elephant is " ++ show e))
The African forest elephant (Loxodonta cyclotis) is a forest-dwelling species of
elephant found in the Congo Basin. For this demonstration we will use Congo
as
an example of something that has an elephant.
data Congo = Congo
{ congoElephant :: Elephant }
We must define the way in which Congo
has an elephant. The lens defines how to
get an elephant from the Congo, and how to put an elephant into the Congo.
instance HasElephant Congo
where
elephant :: Lens' Congo Elephant
elephant =
lens
congoElephant
(\a b -> a{ congoElephant = b })
You'll often see people use Template Haskell or generics to generate lenses automatically, but here we defined the lens manually.
Now we can write a program. It has the same context as above: elephant state and
I/O. Our program prints the value of elephant
, then changes the value of elephant
, then prints it again.
main'
:: (HasElephantState s m, MonadIO m)
=> m ()
main' =
do
printElephant
setElephant 2
printElephant
Then we can convert this program to IO
. We specialize main'
as StateT Congo
IO ()
, and we use a Congo with elephant zero as the program's initial state.
main :: IO ()
main =
let
program =
main' :: StateT Congo IO ()
initialState =
Congo{ congoElephant = 0 }
in
() <$ runStateT program initialState
When we run this program, its output is:
The current elephant is 0
The current elephant is 2
You can find this code on GitHub.