有两种解决方案,我将使用一个示例程序来演示它们。让我们以下面的简单配置文件为例:
-- config.hs
data Config = Config { firstName :: String, lastName :: String }
deriving (Read, Show)
让我们将其加载ghci
到生成一些快速示例文件:
$ ghci config.hs
>>> let config = Config "Gabriel" "Gonzalez"
>>> config
Config {firstName = "Gabriel", lastName = "Gonzalez"}
>>> writeFile "config.txt" config
>>> ^D
现在让我们定义一个读取这个配置文件并打印它的程序:
-- config.hs
data Config = Config { firstName :: String, lastName :: String }
deriving (Read, Show)
main = do
config <- fmap read $ readFile "config.txt" :: IO Config
print config
让我们确保它有效:
$ runhaskell config.hs
Config {firstName = "Gabriel", lastName = "Gonzalez"}
现在,让我们修改程序以漂亮地打印名称,尽管是以一种人为的方式。以下程序演示了配置传递的第一种方法:将配置作为普通参数传递给需要它的函数。
-- 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
这是最轻量级的方法。但是,有时对于非常大的程序来说,所有手动参数传递都会变得乏味。幸运的是,有一个 monad 可以为您处理参数传递,称为Reader
monad。你给它一个“环境”,比如我们的config
变量,它会将该环境作为只读变量传递给Reader
monad 中的任何函数都可以访问的变量。
以下程序演示了如何使用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)
请注意,我们如何只config
在调用 的地方传递一次变量runReader
,并且该例程中的每个函数都可以像只读全局变量一样访问它,使用ask
orasks
函数。同样,请注意当pretty
调用时helper
,它不再需要config
作为参数传递给helper
。Reader
monad 在后台自动为你做这件事。
重要的是要强调Reader
monad 不使用任何副作用来执行此操作。Reader
monad在底层转换为一个纯函数,它只是手动传递参数,就像我们之前在第一个示例中所做的那样。它只是为我们自动化了这个过程,所以我们不必这样做。
如果您是 Haskell 的新手,那么我建议您使用第一种方法来练习学习如何使用参数传递来移动信息。Reader
如果您了解它的工作原理以及它如何为您自动传递参数,我只会使用monad,否则如果出现问题,您将不知道如何修复它。
您可能想知道为什么我没有提到IORef
s 作为传递全局变量的方法。原因是即使你定义了一个IORef
引用来保存你的变量,你仍然必须传递IORef
它本身才能让下游函数能够访问它,所以使用IORef
s. 与主流语言不同,Haskell 强制每个函数声明它从哪里获取信息,无论它是作为普通参数:
foo :: Config -> ...
...或作为Reader
单子:
bar :: Reader Config ...
...或作为可变参考:
baz :: IORef Config -> IO ...
这是一件好事,因为这意味着您始终可以检查一个函数并了解它有哪些可用信息,更重要的是,它没有哪些信息可用。这使得调试函数更容易,因为函数的类型总是明确定义函数所依赖的所有内容。