我是Haskell的新手,但了解如何使用Monad变形金刚。 然而,我仍然难以获得他们声称的优势,而不是将参数传递给函数调用。
基于wiki Monad Transformers Explained,我们基本上将Config对象定义为
data Config = Config Foo Bar Baz
并传递它,而不是使用此签名编写函数
client_func :: Config -> IO ()
我们使用ReaderT Monad Transformer并将签名更改为
client_func :: ReaderT Config IO ()
拉动配置只需拨打ask
。
函数调用从client_func c
更改为runReaderT client_func c
精细。
但为什么这会让我的应用程序更简单?
1-我怀疑当你将很多功能/模块组合在一起形成一个应用程序时,Monad变形金刚会感兴趣。但这就是我的理解停止的地方。有人可以请一些亮点吗?
2-我找不到任何关于如何在Haskell中编写大型模块化应用程序的文档,其中模块公开某种形式的API并隐藏它们的实现,以及(部分)隐藏自己的来自其他模块的状态和环境。有什么指示吗?
(编辑:真实世界Haskell声称“......这种方法[Monad变形金刚] ......扩展到更大的程序。”,但没有明确的例子证明这种说法)
编辑关注Chris Taylor以下
Chris完美地解释了为什么在Transformer Monad中封装Config,State等等有两个好处:
getUserInput
函数)Writer
以提供更低级别的功能)这是以更改所有功能的签名为代价的,以便它们在Transformer Monad中“运行”。
因此问题1已完全涵盖。谢谢Chris。
问题2现已在this SO post
中得到解答答案 0 :(得分:47)
假设我们正在编写一个需要以下列形式的配置信息的程序:
data Config = C { logFile :: FileName }
编写程序的一种方法是在函数之间显式传递配置。如果我们只需要将它传递给明确使用它的函数就好了,但遗憾的是我们不确定函数是否需要调用另一个使用该配置的函数,所以我们不得不将它作为一个函数传递给它。参数无处不在(实际上,它往往是需要使用配置的低级函数,这迫使我们将它传递给所有高级函数)。
让我们编写这样的程序,然后我们将使用Reader
monad重新编写它,看看我们得到了什么好处。
我们最终得到类似的东西:
readLog :: Config -> IO String
readLog (C logFile) = readFile logFile
writeLog :: Config -> String -> IO ()
writeLog (C logFile) message = do x <- readFile logFile
writeFile logFile $ x ++ message
getUserInput :: Config -> IO String
getUserInput config = do input <- getLine
writeLog config $ "Input: " ++ input
return input
runProgram :: Config -> IO ()
runProgram config = do input <- getUserInput config
putStrLn $ "You wrote: " ++ input
请注意,在高级函数中,我们必须始终传递配置。
另一种方法是使用Reader
monad重写。这使得低级功能变得复杂:
type Program = ReaderT Config IO
readLog :: Program String
readLog = do C logFile <- ask
readFile logFile
writeLog :: String -> Program ()
writeLog message = do C logFile <- ask
x <- readFile logFile
writeFile logFile $ x ++ message
但作为我们的奖励,高级功能更简单,因为我们永远不需要参考配置文件。
getUserInput :: Program String
getUserInput = do input <- getLine
writeLog $ "Input: " ++ input
return input
runProgram :: Program ()
runProgram = do input <- getUserInput
putStrLn $ "You wrote: " ++ input
我们可以将getUserInput和runProgram的类型签名重写为
getUserInput :: (MonadReader Config m, MonadIO m) => m String
runProgram :: (MonadReader Config m, MonadIO m) => m ()
如果我们因为任何原因决定要更改基础Program
类型,那么为我们提供了很大的灵活性。例如,如果我们想在程序中添加可修改状态,我们可以重新定义
data ProgramState = PS Int Int Int
type Program a = StateT ProgramState (ReaderT Config IO) a
我们根本不需要修改getUserInput
或runProgram
- 它们会继续正常工作。
N.B。我没有打字查看这篇文章,更不用说试图运行它了。可能有错误!