The Preferences problem (or runtime configuration changes)
Global state in Standard ML
$ sml
Standard ML of New Jersey v110.78 [built: Sat Dec 27 20:53:42 2014]
- val v = ref 5 ;
val v = ref 5 : int ref
- val addToV = fn w => !v + w ;
val addToV = fn : int -> int
- addToV 5 ;
val it = 10 : int
- fun incrV () = v := !v +1 ;
val incrV = fn : unit -> unit
- incrV () ;
val it = () : unit
- addToV 5 ;
val it = 11 : int
The use of global variables in functions breaks the Referential transparency principle that states that if a function call with the same arguments gives you always the same result (it depends only on the arguments) is a pure function and your program will be more predictable and easier to debug.
Proper global variables in Haskell
You may create Haskell global variables with Data.IORef.newIORef as part of IO effects in the IO Monad. Then, to use them elsewhere, you will have to pass them as parameters, keeping the Referential transparency of the functions that use them:
{-# LANGUAGE PackageImports #-}
import Data.IORef
import Control.Monad (when)
import System.IO (hFlush, stdout)
import "safe" Safe (readMay)
default (Int) -- literal desambiguation
main :: IO ()
main = do
ref <- newIORef 0
loop ref
loop :: IORef Int -> IO ()
loop ref = do
w <- readIORef ref
putStrLn $ "the variable is: " ++ show w
putStr "input a positive value to add (else the program will end): "
hFlush stdout
str <- getLine
case (readMay str :: Maybe Int) of
Nothing -> do
putStrLn "unreadable entry!!"
loop ref
Just v -> when (v > 0) $ do -- repeat if v > 0, modifying the variable with (v+)
modifyIORef ref (v+)
loop ref
-- else finish the loop
Encoding the global state in a Monad - The Reader Monad
Here the global state will be called "the environment".
Since the monad result depends on the environment, the monad data should host this relation as a function.
newtype Reader env a = Reader { runReader :: (env -> a) }
class MonadReader env m | m -> env where
ask :: m env -- get the environment
local :: (env -> env) -> m a -> m a -- evaluate an action with a modified environment
asks :: MonadReader env m => (env -> a) -> m a -- retrieve a projection on the environment
asks env_selector = ask >>= (\env -> return $ env_selector env)
We will use the ReaderT monad transformer
With a simplified environment
{-# LANGUAGE PackageImports #-}
import "mtl" Control.Monad.Reader (ReaderT( runReaderT), MonadReader( ask, local))
import "mtl" Control.Monad.Trans (liftIO)
import Control.Monad (when)
import System.IO (hFlush, stdout)
import "safe" Safe (readMay)
default (Int) -- literal desambiguation
type Env = Int -- simplified environment
initial_env = 0
main :: IO ()
main = (runReaderT loop) initial_env -- apply the monad content (a function) to the initial environment
loop :: ReaderT Env IO ()
loop = do
env <- ask -- get the actual environment
str <- liftIO $ do
putStrLn $ "the environment is: " ++ show env
putStr "input a positive value to add (else the program will end): "
hFlush stdout
getLine
case (readMay str :: Maybe Int) of
Nothing -> do
liftIO $ putStrLn "unreadable entry!!"
loop
Just v -> -- repeat only if v > 0, modifying the environment with (v+)
when (v > 0) $ local (v+) loop
With a multifield environment and lenses
I propose profunctor lenses, as an easy way to build component updaters.
{-# LANGUAGE PackageImports #-}
import "mtl" Control.Monad.Reader (ReaderT( runReaderT), MonadReader( ask, local), asks)
import "mtl" Control.Monad.Trans (liftIO)
import Control.Monad (when)
import System.IO (hFlush, stdout)
import "safe" Safe (readMay)
import "mezzolens" Mezzolens (Lens', get, set)
import "mezzolens" Mezzolens.Unchecked (lens)
default (Int) -- literal desambiguation
data MyEnv = MyEnv {_fld1::Int, _fld2::String}
lensFld1 :: Lens' MyEnv Int
lensFld1 = lens _fld1 (\v env -> env {_fld1 = v})
type Env = MyEnv
initial_env = MyEnv 0 ""
main :: IO ()
main = (runReaderT loop) initial_env -- apply the monad content (a function) to the initial environment
fld1Sel :: MyEnv -> Int
fld1Sel = get lensFld1
envUpdaterOnFld1 :: (Int -> Int) -> MyEnv -> MyEnv
envUpdaterOnFld1 = lensFld1
loop :: ReaderT Env IO ()
loop = do
w <- asks fld1Sel -- get field _fld1 of the environment
str <- liftIO $ do
putStrLn $ "the first part of the environment is: " ++ show w
putStr "input a positive value to add (else the program will end): "
hFlush stdout
getLine
case (readMay str :: Maybe Int) of
Nothing -> do
liftIO $ putStrLn "unreadable entry!!"
loop
Just v -> -- repeat only if v > 0, modifying the environment with (v+)
when (v > 0) $ local (envUpdaterOnFld1 (v+)) loop