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.
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 IORef
s 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 IORef
s. 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With