2

我正在从“Learn you a Haskell for Great Good”教程中学习 Haskell,并且我已经完成了关于writer monads的部分。这是我无法弄清楚的例子。

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) -- shouldn't return (3*5) result in (15,[]) ?

ghci> runWriter $ multWithLog
(15,["Got number: 3","Got number: 5"]) -- how did that happen? 

我试图了解块返回wWriter w amonad中的 monoidic 值是如何do改变的。本教程没有详细说明mappending 是如何发生的。

作为 monad的类型声明Writer和实例声明Writer由教程给出

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

instance (Monoid w) => Monad (Writer w) where  
    return x = Writer (x, mempty)
    (Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')  

如果根据实例声明return x产生结果Writer (x, mempty)并且mempty对于 monoid[a][],不应该return (a*b),等于return (3*5),评估为(15,[])

ghci> return (15) :: Writer [String] Int
WriterT (Identity (15,[]))

我把上面的命令给了 ghci ,它返回一个WriterT类型值,元组包含一个空列表,正如预期的那样。

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

我改用do绑定运算符重写了该块。上面的代码给出了与教程中的原始代码相同的结果。

我的印象是>>=Int 3从结果中提取logNumber 3并将其提供给(\a -> logNumber 5 ...etc.),然后logNumber在不同的值 ( 5) 上执行函数,依此类推。这些操作是如何导致[String]Writer monad 的部分被改变的?

4

2 回答 2

2

从您发布的代码

(Writer (x,v)) >>= f =
   let (Writer (y, v')) = f x in Writer (y, v `mappend` v')

我们可以看到确实f只使用x参数调用。所以在logNumber 3 >>= \a -> ...变量中a确实是必然的3

但是,>>=在调用 之后会做一些事情f,即它v与 结合v'。在您的示例中,vis the[String]来自logNumber 3which is ["Got number: 3"]。取而代之的是用, 和 isv'进行评估。\a -> ...a=3["Got number: 5"]

mappendfor lists is ++,它将列表连接在一起。因此我们得到了最终的结果。

请允许我有点马虎,忽略Writer包装纸。我们得到

return (a*b)
= (a*b, [])

logNumber 5 >>= \b -> return (a*b) 
= logNumber 5 >>= \b -> (a*b, [])
= (5, ["Got number: 5"]) >>= \b -> (a*b, [])
= (a*5, ["Got number: 5"] `mappend` [])
= (a*5, ["Got number: 5"])

logNumber 3 >>= \a -> logNumber 5 >>= \b -> return (a*b)
= logNumber 3 >>= \a -> (a*5, ["Got number: 5"])
= (3, ["Got number: 3"]) >>= \a -> (a*5, ["Got number: 5"])
= (3*5, ["Got number: 3"] `mappend` ["Got number: 5"])
= (15, ["Got number: 3", "Got number: 5"])

直观地说,我们可以假设您的 writer monad 中的一个值是一个有效的计算,它返回一个值(如3),并作为副作用将一些消息附加到字符串列表。所有此类消息的日志在 monad 中是不可见的(我们只能附加到日志中),并且只有在我们将用于runWriter退出 monad 上下文时才可用。

于 2019-03-28T13:14:30.917 回答
1

这应该解释它:

> runWriter (return 15) :: (Int, [String])
(15,[])                       -- == runWriter $ writer (15, mempty)

> runWriter (logNumber 3)
(3,["Got number: 3"])         -- == runWriter $ writer (3, ["Got number: 3"])

> runWriter (logNumber 5)
(5,["Got number: 5"])         -- == runWriter $ writer (5, ["Got number: 5"])

> runWriter (logNumber 3 >> logNumber 5)
(5,["Got number: 3","Got number: 5"])   -- == ["Got number: 3"] ++ ["Got number: 5"]

> runWriter (logNumber 3 >>         logNumber 5 >>         return 15        )
(15,["Got number: 3","Got number: 5"])  -- == ["Got number: 3"] ++ ["Got number: 5"] ++ []

> runWriter (logNumber 3 >>= (\_ -> logNumber 5 >>= (\_ -> return 15    ) ) )
(15,["Got number: 3","Got number: 5"])

> runWriter (logNumber 3 >>= (\i -> logNumber 5 >>= (\j -> return (i*j) ) ) )
(15,["Got number: 3","Got number: 5"])

最后一行的一元表达式等价于multWithLog'do块。

注意 lambda 函数的嵌套:lambda 函数

                                                    (\j -> return (i*j) )

位于lambda 函数

                             (\i -> logNumber 5 >>= (\j -> return (i*j) ) )

这就是为什么iinreturn (i*j)指的是外部lambda 函数的参数i,由它从最外部的一元动作表达式 接收logNumber 3

如何?因为根据>>=您在问题中引用的定义,我们有

   runWriter ( Writer (x,v) >>= f )
=
   runWriter ( let (Writer (y, u)) = f x in Writer (y, v `mappend` u) )
= 
   let (Writer (y, u)) = f x in runWriter ( Writer (y, v `mappend` u) )
= 
   let (Writer (y, u)) = f x in (y, v `mappend` u) 

IE

   runWriter ( logNumber 5 >>= (\j -> return j) )
=                              -------- f -----
   runWriter ( writer (5, ["Got number: 5"]) >>= (\j -> writer (j, mempty)) )
=                  --  x  ------- v -------      -------- f ---------------
   let Writer (y, u) = ( (\j -> writer (j, mempty)) 5 ) 
                         -------- f --------------- x
                            in (y, ["Got number: 5"] `mappend` u) 
=                                  ------- v ------- 
   let (y, u) = (5, mempty) in (y, ["Got number: 5"] `mappend` u) 
=
                               (5, ["Got number: 5"] `mappend` mempty) 

Writer每个动作的“monoidic 值”不会“改变”。每个动作都将其“monoidic 值”贡献Writer给块的组合类型计算的整体“monoidic 值”,由每个子计算贡献的 monoidic 值(的语义do)从其Writer类型子计算构建,发现在表示动作的元组字段中(的实现mappendingWritersndWriter

同样,这个整体值是通过组合每个元组的单一值部分(即它们的snd字段)来组合的。monoidic 组合是,这是通过类型计算mappend在幕后为我们完成的。Writer

对于列表,mappend [a] [b] == [a] ++ [b] == [a,b],而mappend [a,b] [] == [a,b] ++ [] == [a,b]

那么你的问题:

  • 不应该return (a*b) 等于(15,[])

    正如我们在答案开头看到的那样,它应该而且确实如此。

  • WriterWriterT包装器

    没关系。两者都是同构的,因为Identity是无操作的。WriterT是 Writer monad 的实现的一部分;书中给出的更简单,更容易理解。

  • 这些操作是如何导致[String]Writer monad 的部分被改变的?

    mappend不被特定 Writer 使用的特定 Monoid改变,而是组合;作为一元组合的一部分,即一元绑定的>>=定义;作为 Monads 泛化函数调用协议和 Writer Monad 的泛化是在幕后收集 Monoid 值,因此它们可以附加在阴影中,除了用户函数在开放中进行工作:

     do { a <- logNumber 3  
        ; b <- logNumber 5  
        ; return (a*b)
        }
    =    ----- user area ------       ---- hidden area ---
     do {  a       <- writer (3       , ["Got number: 3"]       )
        ;    b     <- writer (5       , ["Got number: 5"]       )  
        ;             writer (
          (a*b)                       , []                      )
        }
    =
     Writer
        ( (3*5)                       , mconcat [..., ..., ...] )
    

Embrace do-notation,它是你的朋友。它帮助我们抽象地思考。

于 2019-03-31T09:39:41.900 回答