【问题标题】:Question about the Writer monad as taught in LYAH. (How did the appending to the log take place?)关于 LYAH 中教授的 Writer monad 的问题。 (附加到日志是如何发生的?)
【发布时间】:2019-08-19 06:25:32
【问题描述】:

我正在从“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? 

我正在尝试了解 do 块返回的 Writer w a monad 中的 monoidic 值 w 是如何改变的。本教程没有详细说明mappending 是如何发生的。

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

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) 并且对于monoid [a]mempty[],则不应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 块。上面的代码给出了与教程中的原始代码相同的结果。

我的印象是&gt;&gt;= 只从logNumber 3 的结果中提取Int 3 并将其提供给(\a -&gt; logNumber 5 ...etc.),然后logNumber 对不同的值(@ 987654348@) 等等。这些操作是如何导致 Writer monad 的 [String] 部分被改变的?

【问题讨论】:

  • 我想你可能对 return 在 Haskell 中的含义感到困惑——这很常见,所以不要为此感到难过。在大多数命令式语言中,函数定义底部的return x 意味着函数的结果是x。但是 Haskell 中的return 只是一个函数,multWithLog 的实际值是通过评估整个do 块获得的表达式,而不仅仅是return 的最后一行。特别是do 块的连续行由&gt;&gt;= 的应用程序连接,正是它在“幕后”附加了日志值。
  • “我的印象是 >>= 只提取 Int 3 ...”。那就是你错了。只有 3 作为 argument 传递给下一个函数,但 m &gt;&gt;= f 不会简单地返回 f 返回的任何内容。它使用mf 的返回值创建一个new Writer 值。

标签: haskell monads monoids do-notation writer-monad


【解决方案1】:

根据您发布的代码

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

我们可以看到确实f 仅使用x 参数调用。 所以在logNumber 3 &gt;&gt;= \a -&gt; ... 变量a 确实绑定到3

然而,&gt;&gt;= 在调用f 之后做了一些事情,即它结合了vv'。在您的示例中,v 是来自logNumber 3[String],即["Got number: 3"]。取而代之的是v' 来自评估\a -&gt; ...a=3,并且是["Got number: 5"]

mappend 对于列表是 ++,它将列表连接在一起。因此我们得到了最终结果。

请允许我有点草率,忽略 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 退出 monadic 上下文时才会提供。

【讨论】:

    【解决方案2】:

    这应该解释一下:

    > 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"])
    

    最后一行的一元表达式等价于multWithLogdo块。

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

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

    位于内部 lambda 函数

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

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

    怎么样?因为根据您在问题中引用的&gt;&gt;=的定义,我们有

       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) 
    

       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 value”到do 块的组合Writer-type 计算的整体“monoidic value”中,由其Writer-type 子计算构建而成mappending 每个子计算贡献的幺半群值(Writer语义),位于表示动作的元组的 snd 字段中( Writer实现)。

    同样,这个整体值是通过组合每个元组的幺半值部分来组合的,即它们的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 的 实现的一部分;书中给出的更简单,更容易理解。

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

      由特定 Writer 使用的特定 Monoid 的 mappend 不改变,而是组合;作为 monadic 组合 的一部分,即 monadic 绑定的 &gt;&gt;= 定义;作为 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 [..., ..., ...] )
      

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

    【讨论】:

      猜你喜欢
      • 2020-09-22
      • 2015-11-29
      • 1970-01-01
      • 1970-01-01
      • 2021-07-14
      • 2014-04-03
      • 1970-01-01
      • 2011-06-20
      • 1970-01-01
      相关资源
      最近更新 更多