101

我是函数式编程的新手,最近在Learn You a Haskell学习,但是当我读完这一章时,我被下面的程序卡住了:

import Control.Monad.Writer  

logNumber :: Int -> Writer [String] Int  
logNumber x = Writer (x, ["Got number: " ++ show x])  

multWithLog :: Writer [String] Int  
multWithLog = do  
    a <- logNumber 3  
    b <- logNumber 5  
    return (a*b)

我将这些行保存在 .hs 文件中,但未能将其导入我的 ghci 中抱怨:

more1.hs:4:15:
    Not in scope: data constructor `Writer'
    Perhaps you meant `WriterT' (imported from Control.Monad.Writer)
Failed, modules loaded: none.

我通过 ":info" 命令检查了类型:

Prelude Control.Monad.Writer> :info Writer
type Writer w = WriterT w Data.Functor.Identity.Identity
               -- Defined in `Control.Monad.Trans.Writer.Lazy'

从我的角度来看,这应该类似于“newtype Writer wa ...”,所以我对如何提供数据构造函数并获取 Writer 感到困惑。

我想这可能是版本相关的问题,我的 ghci 版本是 7.4.1

4

3 回答 3

131

该包Control.Monad.Writer不导出数据构造函数Writer。我想这在 LYAH 被写出来的时候是不同的。

在 ghci 中使用 MonadWriter 类型类

相反,您使用该writer函数创建编写器。例如,在 ghci 会话中,我可以做

ghci> import Control.Monad.Writer
ghci> let logNumber x = writer (x, ["Got number: " ++ show x])

NowlogNumber是一个创建 writer 的函数。我可以询问它的类型:

ghci> :t logNumber
logNumber :: (Show a, MonadWriter [String] m) => a -> m a

这告诉我推断的类型不是返回特定编写器的函数,而是实现MonadWriter类型类的任何东西。我现在可以使用它:

ghci> let multWithLog = do { a <- logNumber 3; b <- logNumber 5; return (a*b) }
    :: Writer [String] Int

(输入实际上在一行上输入)。在这里,我指定了multWithLogto的类型Writer [String] Int。现在我可以运行它了:

ghci> runWriter multWithLog
(15, ["Got number: 3","Got number: 5"])

你会看到我们记录了所有的中间操作。

为什么代码是这样写的?

为什么要费心创建MonadWriter类型类呢?原因与单子变压器有关。正如您正确意识到的那样,最简单的实现方法Writer是作为一对顶部的新类型包装器:

newtype Writer w a = Writer { runWriter :: (a,w) }

您可以为此声明一个 monad 实例,然后编写函数

tell :: Monoid w => w -> Writer w ()

它只是记录其输入。现在假设您想要一个具有日志记录功能的 monad,但还可以执行其他操作 - 假设它也可以从环境中读取。您将其实现为

type RW r w a = ReaderT r (Writer w a)

现在因为 writer 在ReaderTmonad 转换器中,如果你想记录你不能使用的输出tell w(因为它只与未包装的 writer 一起使用)但你必须使用lift $ tell w,它通过“提升”tell函数ReaderT以便它可以访问内心作家单子。如果您想要两层转换器(假设您还想添加错误处理),那么您需要使用lift $ lift $ tell w. 这很快变得笨拙。

相反,通过定义一个类型类,我们可以将编写器周围的任何 monad 转换器包装器变成编​​写器本身的实例。例如,

instance (Monoid w, MonadWriter w m) => MonadWriter w (ReaderT r m)

也就是说,如果w是一个幺半群,并且m是一个MonadWriter w,那么ReaderT r m也是一个MonadWriter w。这意味着我们可以tell直接在转换后的 monad 上使用该函数,而不必费心通过 monad 转换器显式提升它。

于 2012-07-27T08:56:07.363 回答
9

一个名为“writer”的函数可以代替“Writer”构造函数。改变:

logNumber x = Writer (x, ["Got number: " ++ show x])

到:

logNumber x = writer (x, ["Got number: " ++ show x])

于 2016-05-18T01:27:22.743 回答
1

我在 repl.it中使用在线 Haskell 编辑器尝试LYAH “For a few Monads More”时得到了类似的信息

我将导入更改为:

import Control.Monad.Writer

到:

import qualified Control.Monad.Trans.Writer.Lazy as W

所以我的代码现在看起来像这样(灵感来自Kwang 的 Haskell 博客):

import Data.Monoid
import qualified Control.Monad.Trans.Writer.Lazy as W


output :: String -> W.Writer [String] ()
output x = W.tell [x]


gcd' :: Int -> Int -> W.Writer [String] Int  
gcd' a b  
    | b == 0 = do  
        output ("Finished with " ++ show a)
        return a  
    | otherwise = do  
        output (show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b))
        gcd' b (a `mod` b)

main :: IO()
main = mapM_ putStrLn $ snd $ W.runWriter (gcd' 8 3) 

代码目前可在此处运行

于 2018-04-24T04:46:00.817 回答