【问题标题】:What are design patterns for tasks with storing some state in haskell在haskell中存储一些状态的任务的设计模式是什么
【发布时间】:2012-12-25 10:50:14
【问题描述】:

在 haskell 中存储某些状态的任务的设计模式是什么?例如,我想用 haskell 编写库,它提供配置文件读取并将配置参数存储在内存中。

例如:

我有配置文件。配置文件的语法现在并不重要。我阅读了配置文件,将其解析为一些 haskell 数据结构。接下来我想退出使用这个库从 config.xml 获取参数的程序。我们在 haskell 中没有全局变量。我不想要每次都会读取和解析配置的调用函数。我想读取一次配置,而不是多次获取参数。

对于这些类型的问题,haskell 中存在哪些常见做法?

谢谢。

【问题讨论】:

  • "如果你有一个全局环境,各种函数从其中读取(例如,你可能从配置文件初始化),那么你应该将它作为参数传递给你的函数(在拥有之后,很有可能,在你的'主要'动作中设置它)。如果传递的显式参数让你烦恼,那么你可以用 Monad '隐藏'它。 haskell.org/haskellwiki/Global_variables
  • 这听起来像是 Monad Reader 的工作

标签: design-patterns haskell state


【解决方案1】:

有两种解决方案,我将使用一个示例程序来演示它们。我们以下面这个简单的配置文件为例:

-- 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)

请注意,我们只在调用runReader 时传递config 变量一次,并且该例程中的每个函数都可以像只读全局变量一样访问它,使用ask 或@ 987654335@ 功能。同样,请注意当pretty 调用helper 时,它不再需要将config 作为参数传递给helperReader monad 会在后台自动为您执行此操作。

重要的是要强调Reader monad 不使用任何副作用来执行此操作。 Reader monad 在底层转换为纯函数,它只是手动传递参数,就像我们之前在第一个示例中所做的那样。它只是为我们自动执行此过程,因此我们不必这样做。

如果您是 Haskell 新手,那么我建议您使用第一种方法来练习学习如何使用参数传递来移动信息。如果您了解 Reader monad 的工作原理以及它如何为您自动传递参数,我只会使用它,否则如果出现问题,您将不知道如何修复它。

您可能想知道为什么我没有提到IORefs 作为一种传递全局变量的方法。原因是即使你定义了一个 IORef 引用来保存你的变量,你仍然必须传递 IORef 本身才能让下游函数能够访问它,所以使用 @987654347 你什么也得不到@s。与主流语言不同,Haskell 强制每个函数声明它从哪里获取信息,无论它是作为普通参数:

foo :: Config -> ...

...或作为Reader monad:

bar :: Reader Config ...

...或作为可变引用:

baz :: IORef Config -> IO ...

这是一件好事,因为这意味着您可以随时检查函数并了解它有哪些可用信息,更重要的是,它没有哪些信息可用。这使得调试函数更容易,因为函数的类型总是明确定义函数所依赖的所有内容。

【讨论】:

  • 顺便说一下,当您确实需要 IORefs 用于其他目的时,将 IORefs 本身存储到 ReaderT 环境中是提供本地访问有限数量的“全局变量”。我经常在并发代码中使用这种风格,使用TVar 而不是IORef
【解决方案2】:

嗯,一种做法可能是configurator 中使用的。 blog post 刚刚介绍了如何使用它的概述。您可以深入研究实现,看看什么对您的项目有用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-12
    • 1970-01-01
    • 1970-01-01
    • 2016-03-29
    相关资源
    最近更新 更多