【发布时间】:2013-09-20 05:45:56
【问题描述】:
我在 mtl 库中查找内容时遇到了 RWS Monad 及其 MonadTransformer。那里没有真正的文档,我想知道这是什么以及它在哪里使用。
我已经发现 RWS 是 Reader、Writer、State 的首字母缩写词,它是这三个 monad 转换器的堆栈。我无法弄清楚为什么这比 State 本身更好。
【问题讨论】:
标签: haskell monad-transformers
我在 mtl 库中查找内容时遇到了 RWS Monad 及其 MonadTransformer。那里没有真正的文档,我想知道这是什么以及它在哪里使用。
我已经发现 RWS 是 Reader、Writer、State 的首字母缩写词,它是这三个 monad 转换器的堆栈。我无法弄清楚为什么这比 State 本身更好。
【问题讨论】:
标签: haskell monad-transformers
这样做的最实际原因是为了可测试性和更精确的类型签名。
haskell 的关键优势在于您可以如何通过类型系统指定函数的功能。比较c#/java类型:
public int CSharpFunction(int param) { ...
使用haskell:
someFunction :: Int -> Int
haskell 不仅告诉我们参数所需的类型和返回类型,还告诉我们函数可能会影响什么。例如,它不能做任何 IO,也不能读取或更改任何全局状态,或访问任何静态配置数据。对于 c# 函数,两者都可能不正确,但我们无法确定。
这对测试很有帮助。我们知道,我们需要用 haskell someFunction 测试的唯一事情是它是否获得了某些样本输入的预期输出。不需要任何可能的设置,并且该函数永远不会为相同的输入给出不同的结果。这使得使用纯函数进行测试变得非常简单。
但是,函数通常不能是纯函数。例如,它可能需要访问一些全局信息只是为了阅读。我们可以在函数中添加另一个参数:
readerFunc :: GlobalConfig -> Int -> Int
但使用 monad 通常更容易,因为它们更容易编写:
readerFunc2 :: Int -> Reader GlobalConfig Int
测试这个几乎和测试一个纯函数一样简单。我们只需要测试输入 Int 值和 GlobalConfig 读取器配置值的各种排列。
一个函数可能需要写出值(例如用于记录)。这也可以通过 monad 来完成:
writerFunc :: Int -> Writer String Int
再次测试这几乎与纯函数一样容易。我们只是测试对于给定的Int 输入,是否返回了适当的Int,以及正确的最终作者String。
另一个函数可能需要读取和改变状态:
stateFunc :: Int -> State GlobalState Int
但这更难测试。我们不仅要使用各种输入 Ints 和初始 GlobalStates 检查输出,还需要测试最终 GlobalState 是否是正确的值。
现在考虑一个函数:
ProgramState 值。我们可以这样做:
data ProgramData = ProgramData { pState :: ProgramState, pConfig :: ProgramConfig, pLogs :: String }
complexFunction :: Int -> State ProgramData Int
但是,这种类型不是很准确。这意味着 ProgramConfig 可能会更改,但不会更改。这也意味着该函数可以使用 pLogs 值,但它不会。此外,测试它更复杂,理论上,该函数可能会意外更改程序配置,或在其计算中使用当前 pLogs 值。
这可以大大改善:
betterFunction :: Int -> RWS ProgramConfig String ProgramState Int
这种类型非常准确。我们知道 ProgramConfig 只会被读取。我们知道String 永远不会改变。正如预期的那样,唯一需要读写的部分是ProgramState。这更容易测试,因为我们只需要测试 ProgramConfig、ProgramState 和Int 的不同组合作为输入,并检查输出Int、String 和ProgramState 的输出。我们知道我们不能意外更改程序配置,或在计算中使用当前程序日志值。
haskell 类型系统可以帮助我们,我们应该为它提供尽可能多的信息,以便它能够在我们之前捕获错误。
(请注意,此答案中的每个 haskell 类型都可能等同于顶部的 c# 类型,具体取决于 c# 函数的实际实现方式)。
【讨论】:
Config -> Int -> State -> (Int, State, Log)?
Int -> RWS Config Log State Int,对于一个简单的程序来说非常好。然而,monadic 版本最终更容易用于更复杂的程序,因为您不必担心将正确的状态值传递给所有相关函数,也不必担心在最后构建和返回正确的日志值。
RWS,您可以访问具有MonadReader、MonadWriter 和MonadState 类型的内容,这些内容可能是由不知道您在应用程序中使用什么类型的其他人编写的。此外,在定义 RWS 类型的值时,您不需要提供参数,其中一些甚至可能不会使用(因为并非每个函数都使用配置、状态和日志)。如果您以后想要更多/不同的功能,则需要修改所有定义。将 RWS 更改为更强大的 monad 不需要任何此类更改 ...
状态是一个非常笼统的概念,因此可以用于许多事情。 Reader 和 Writer 可以被认为是具有某些约束的特殊形式的 State,您只能从 reader 读取,而只能向 writer 写入。使用这些特殊的状态形式,您可以更明确了解您要达到的目标或确切的意图。
另一个类比是使用地图/字典对任何事物(对象、数据、事件处理程序等)进行建模,但使用更专业的地图/字典形式会使事情更加明确
【讨论】: