Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are design patterns for tasks with storing some state in haskell

What are design patterns for tasks with storing some state in haskell? For example i want to write library with haskell which provide config file reading and storing config params in memory.

For example:

I have file with configuration. Syntax of config file is not important now. I read config file, parse it to some haskell data structure. Next I want out of my program that uses this library to obtain the parameters from config. We have no global variables in haskell. I don't want call function which will read and parse config every time. I want read config one time and than get params many time.

What exists is a common practice for these types of problems in haskell?

Thank you.

like image 639
0xAX Avatar asked Dec 07 '22 10:12

0xAX


2 Answers

There are two solutions, and I will use a sample program to demonstrate both of them. Let's use the following simple configuration file as an example:

-- config.hs

data Config = Config { firstName :: String, lastName :: String }
    deriving (Read, Show)

Let's load that up into ghci to generate some a quick sample file:

$ ghci config.hs
>>> let config = Config "Gabriel" "Gonzalez"
>>> config
Config {firstName = "Gabriel", lastName = "Gonzalez"}
>>> writeFile "config.txt" config
>>> ^D

Now let's define a program that reads in this configuration file and prints it:

-- config.hs
data Config = Config { firstName :: String, lastName :: String }
    deriving (Read, Show)

main = do
    config <- fmap read $ readFile "config.txt" :: IO Config
    print config

Let's make sure it works:

$ runhaskell config.hs
Config {firstName = "Gabriel", lastName = "Gonzalez"}

Now, let's modify the program to pretty print the name, albeit in a contrived way. The following program demonstrates the first approach to configuration passing: Pass the configuration as an ordinary parameter to functions that need it.

-- config.hs
data Config = Config { firstName :: String, lastName :: String }
    deriving (Read, Show)

main = do
    config <- fmap read $ readFile "config.txt" :: IO Config
    putStrLn $ pretty config

pretty :: Config -> String
pretty config = firstName config ++ helper config

helper :: Config -> String
helper config = " " ++ lastName config

This is the most light-weight approach. However, sometimes all that manual parameter passing can get tedious for very large programs. Fortunately, there is a monad that takes care of parameter passing for you, known as the Reader monad. You give it an "environment", such as our config variable, and it passes that environment around as a read-only variable that any function in the Reader monad can access.

The following program demonstrates how to use the Reader monad:

-- config.hs

import Control.Monad.Trans.Reader -- from the "transformers" package

data Config = Config { firstName :: String, lastName :: String }
    deriving (Read, Show)

main = do
    config <- fmap read $ readFile "config.txt" :: IO Config
    putStrLn $ runReader pretty config

pretty :: Reader Config String
pretty = do
    name1 <- asks firstName
    rest  <- helper
    return (name1 ++ rest)

helper :: Reader Config String
helper = do
    name2 <- asks lastName
    return (" " ++ name2)

Notice how we only pass the config variable once at the point where we call runReader, and every function within that routine has access to it like a read-only global variable, using either the ask or asks functions. Similarly, notice how when pretty calls helper, it doesn't need to pass config as a parameter to helper any longer. The Reader monad does that for you automatically in the background.

It's important to emphasize that the Reader monad doesn't use any side effects to do this. The Reader monad translates to a pure function under the hood that just passes around the parameters manually the same way we were doing before in the first example. It just automates this process for us so we don't have to do it.

If you are new to Haskell then I recommend the first approach to exercise learning how to use parameter passing to move information around. I would only use the Reader monad if you understand how it works and how it automates that parameter passing for you, otherwise if things go wrong you won't know how to fix it.

You might have wondered why I didn't mention IORefs as an approach for passing global variables around. The reason why is that even if you define an IORef reference to hold your variable, you still have to pass around the IORef itself in order for downstream functions to be able to access it, so you gain nothing by using IORefs. Unlike a mainstream language, Haskell forces every function declare where it gets its information from, whether it is as an ordinary parameter:

foo :: Config -> ...

... or as a Reader monad:

bar :: Reader Config ...

... or as a mutable reference:

baz :: IORef Config -> IO ...

This is a good thing, because it means you can always inspect a function and understand what information it has available and, more importantly, what information it does NOT have available. This makes it easier to debug functions because a function's type always explicitly defines everything that function depends on.

like image 166
Gabriella Gonzalez Avatar answered Dec 09 '22 15:12

Gabriella Gonzalez


Well, one practice may be what is used in configurator. An overview of how to use this was just covered in a blog post. You could delve into the implementation to see what might work for your project.

like image 37
Chris Kuklewicz Avatar answered Dec 09 '22 13:12

Chris Kuklewicz