Haskell Monad变压器与向函数传递参数

Haskell Monad变压器与向函数传递参数,haskell,monad-transformers,Haskell,Monad Transformers,我是Haskell的新手,但了解Monad变压器的使用方法。 然而,我仍然很难抓住它们声称的相对于将参数传递给函数调用的优势 基于wiki,我们基本上有一个定义为 data Config = Config Foo Bar Baz 并传递它,而不是用这个签名编写函数 client_func :: Config -> IO () 我们使用ReaderT Monad转换器并将签名更改为 client_func :: ReaderT Config IO () 然后,拉动配置只需调用ask 函

我是Haskell的新手,但了解Monad变压器的使用方法。 然而,我仍然很难抓住它们声称的相对于将参数传递给函数调用的优势

基于wiki,我们基本上有一个定义为

data Config = Config Foo Bar Baz
并传递它,而不是用这个签名编写函数

client_func :: Config -> IO ()
我们使用ReaderT Monad转换器并将签名更改为

client_func :: ReaderT Config IO ()
然后,拉动配置只需调用
ask

函数调用从
client\u func c
更改为
runReaderT client\u func c

好的

但是为什么这会使我的应用程序更简单呢

1-我怀疑Monad Transformers对将许多功能/模块缝合在一起形成应用程序感兴趣。但这就是我的理解停止的地方。谁能帮我弄点光吗

2-我找不到任何关于如何在Haskell中编写大型模块化应用程序的文档,其中模块公开某种形式的API并隐藏其实现,以及(部分)对其他模块隐藏其自身状态和环境。有什么建议吗

(编辑:现实世界的哈斯克尔说,“.这种方法[单子变形金刚]…可以扩展到更大的程序”,但没有明确的例子证明这种说法)

编辑下面的Chris Taylor答案

Chris完美地解释了为什么封装配置、状态等。。。在Transformer中,Monad提供了两个好处:

  • 它可以防止更高级别的函数必须在其类型签名中保留它调用的(子)函数所需但不需要用于自身使用的所有参数(请参见
    getUserInput
    函数)
  • 因此,更高级别的函数对Transformer Monad内容的更改更具弹性(例如,您希望向其添加一个
    Writer
    ,以便在较低级别的函数中提供日志记录)
  • 这是以更改所有函数的签名为代价的,以便它们“在”Transformer Monad中运行

    因此,问题1已完全涵盖。谢谢你,克里斯


    问题2现在在

    中得到了回答,假设我们正在编写一个程序,它需要以下形式的一些配置信息:

    data Config = C { logFile :: FileName }
    
    编写程序的一种方法是在函数之间显式地传递配置。如果我们只需要将它显式地传递给使用它的函数就好了,但遗憾的是,我们不确定一个函数是否需要调用另一个使用配置的函数,因此我们不得不将它作为参数传递到任何地方(事实上,需要使用配置的往往是低级功能,这迫使我们也将配置传递给所有高级功能)

    让我们这样编写程序,然后我们将使用
    阅读器
    monad重新编写它,看看我们得到了什么好处

    选项1.显式配置传递 我们的结局是这样的:

    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
    
    但是作为奖励,高级函数更简单,因为我们不需要引用配置文件

    getUserInput :: Program String
    getUserInput = do input <- getLine
                      writeLog $ "Input: " ++ input
                      return input
    
    runProgram :: Program ()
    runProgram = do input <- getUserInput
                    putStrLn $ "You wrote: " ++ input
    
    如果我们决定出于任何原因更改底层
    程序
    类型,这为我们以后的工作提供了很大的灵活性。例如,如果我们想向程序添加可修改状态,我们可以重新定义

    data ProgramState = PS Int Int Int
    
    type Program a = StateT ProgramState (ReaderT Config IO) a
    
    而且我们根本不需要修改
    getUserInput
    runProgram
    ——它们将继续正常工作


    注意:我还没有检查过这篇文章,更不用说试着运行它了。可能会有错误!

    当你想暂时使用某些monad的功能进行子计算时,转换器也很有用。例如,我用
    writer
    记录信息,同时用
    状态生成的唯一标识符更新数据结构。谢谢你,克里斯。它非常有意义,完美地描述了“隐藏效果”的好处。它还提出了一些关于大型应用程序中签名应遵守的规则函数以及应用程序的一般结构的附加问题。我建议让这个问题运行几个小时,看看我们是否能获得更多的输入。然后我会来返回,对任何剩余的澄清请求进行更好的形式化。它应该是
    getUserInput::(MonadReader配置m,MonadIO m)=>m字符串
    (并且您需要启用
    FlexibleContexts
    )。此外,泛化函数调用的所有函数也需要泛化类型。泛化类型比特定类型大,并且不能将大类型放入小类型(但是,当您使用特定的
    Monad
    调用
    runProgram
    时,可以将小类型放入顶部的大类型)@alinsoar我认为最好的monad教程是“你本可以发明monad(也许你已经发明了)”,这是可以找到的。同一位作者也有一篇文章,我似乎记得这篇文章写得很好。@alinsoar Scala有monad(称为理解),你也可以在OCaml中使用它们(使用模块而不是类型类)虽然我认为语法不是很好,Python有列表理解和生成器理解,您可以将其视为列表monad的实现(尽管它也允许您执行IO)。正如你所说,你可以用任何语言实现Monad,但是没有类型类、参数多态性和内置语法,这有点笨拙。@alinsoar获得更多经验的唯一方法是获得更多经验。理解任何编程语言都没有神奇的途径。只需使用该语言解决问题、阅读书籍或关于它的文章和论文,并询问/回答有关堆栈溢出的问题。理解将会到来。
    getUserInput :: (MonadReader Config m, MonadIO m) => m String
    
    runProgram :: (MonadReader Config m, MonadIO m) => m ()
    
    data ProgramState = PS Int Int Int
    
    type Program a = StateT ProgramState (ReaderT Config IO) a