怎么和 Control Monad Haskell 的作家玩?

我刚接触函数式编程,最近在 向你学习 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'

在我看来,这应该是类似于“新型作家”的东西 所以我对如何提供数据构造函数和获得 Writer 感到困惑。

我想这可能是一个版本相关的问题,我的 ghci 版本是7.4

15629 次浏览

The package Control.Monad.Writer does not export the data constructor Writer. I guess this was different when LYAH was written.

Using the MonadWriter typeclass in ghci

Instead, you create writers using the writer function. For example, in a ghci session I can do

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

Now logNumber is a function that creates writers. I can ask for its type:

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

Which tells me that the inferred type is not a function that returns a particular writer, but rather anything that implements the MonadWriter type class. I can now use it:

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

(Input actually entered all on one line). Here I've specified the type of multWithLog to be Writer [String] Int. Now I can run it:

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

And you see that we log all of the intermediate operations.

Why is the code written like this?

Why bother to create the MonadWriter type class at all? The reason is to do with monad transformers. As you correctly realised, the simplest way to implement Writer is as a newtype wrapper on top of a pair:

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

You can declare a monad instance for this, and then write the function

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

which simply logs its input. Now suppose you want a monad that has logging capabilities, but also does something else - say it can read from an environment too. You'd implement this as

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

Now because the writer is inside the ReaderT monad transformer, if you want to log output you can't use tell w (because that only operates with unwrapped writers) but you have to use lift $ tell w, which "lifts" the tell function through the ReaderT so that it can access the inner writer monad. If you wanted two layers transformers (say you wanted to add error handling as well) then you'd need to use lift $ lift $ tell w. This quickly gets unwieldy.

Instead, by defining a type class we can make any monad transformer wrapper around a writer into an instance of writer itself. For example,

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

that is, if w is a monoid, and m is a MonadWriter w, then ReaderT r m is also a MonadWriter w. This means that we can use the tell function directly on the transformed monad, without having to bother with explicitly lifting it through the monad transformer.

A function called "writer" is made available in lieu of a "Writer" constructor. Change:

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

to:

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

I got a similar message from trying the LYAH "For a few Monads More" using the online Haskell editor in repl.it

I changed the import from:

import Control.Monad.Writer

to:

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

So my code now works looking like this (with inspiration from Kwang's Haskell Blog):

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)

Code is currently runable here