【问题标题】:Understanding this assignment?理解这个任务?
【发布时间】:2018-06-12 21:14:51
【问题描述】:

为了刷新我 20 年使用 Haskell 的经验,我正在浏览 https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/Adding_Variables_and_Assignment 并在某一时刻引入以下行以将 op 应用于所有参数。这是为了实现例如(+ 1 2 3 4)

numericBinop op params = mapM unpackNum params >>= return . Number . foldl1 op

语法看不懂,文中的解释有点模糊。

我了解 foldl1 的作用以及如何点函数(unpackNum 是一个辅助函数),但是使用 Monads 和 >>= 运算符让我有点困惑。

如何阅读?

【问题讨论】:

  • 阅读本文时您有什么特别困惑的地方?如果您要求对>>= 进行解释,这可能不是这个地方。
  • @AJFarmar 我很难从以前的版本(它更简单,因为它没有在 monad 中进行错误处理)飞跃到甚至理解这些指令的含义。

标签: haskell syntax scheme monads interpreter


【解决方案1】:

基本上,

mapM unpackNum params >>= return . Number . foldl1 op

由两部分组成。

mapM unpackNum params 表示:获取参数列表params。在每个项目上,应用unpackNum:这将产生一个包裹在ThrowsError monad 中的Integer。所以,它不完全是一个普通的Integer,因为它有可能出错。无论如何,在每个项目上使用unpackNum 要么成功生成所有Integers,要么引发一些错误。在第一种情况下,我们生成一个[Integer] 类型的新列表,在第二种情况下,我们(不出所料)抛出错误。所以,这部分的结果类型是ThrowsError [Integer]

第二部分是... >>= return . Number . foldl1 op。这里>>= 的意思是:如果第一部分抛出了一些错误,那么整个表达式也会抛出那个错误。如果该部分成功生成[Integer],则继续foldl1 op,将结果包装为Number,最后使用return 注入此值作为成功计算。

总的来说,有单子计算,但你不应该考虑太多。这里的 monadic 东西只是传播错误,或者如果计算成功则存储纯值。有了一点经验,就可以只关注成功的值,而让mapM,>>=,return处理错误情况。

顺便提一下,虽然本书使用了action >>= return . f 之类的代码,但这可以说是一种糟糕的风格。可以使用fmap f actionf <$> action 达到同样的效果,这是表达相同计算的一种更直接的方式。例如

Number . foldl1 op <$> mapM unpackNum params

这非常接近于忽略错误情况的非单子代码

-- this would work if there was no monad around, and errors did not have to be handled
Number . foldl1 op $ map unpackNum params

【讨论】:

    【解决方案2】:

    您的问题是关于语法的,所以我将只讨论如何解析该表达式。 Haskell 的语法非常简单。非正式地:

    • 用空格分隔的标识符是函数应用(第一个标识符应用到其余部分)
    • 除了使用非字母数字字符的标识符(例如 &gt;&gt;=.)是中缀(即它们的第一个参数在标识符的左侧)
    • 上述第一种类型的函数应用程序(非中缀)比第二种更紧密地绑定
    • 运算符可以关联到左侧或右侧,并且具有不同的优先级(使用infix... 声明定义)

    所以只有当我看到时才知道:

    mapM unpackNum params >>= return . Number . foldl1 op
    

    首先我知道它必须像这样解析

    (mapM unpackNum params) >>= return . Number . (foldl1 op)
    

    为了更进一步,我们需要检查我们在这个表达式中看到的两个运算符的固定性/优先级:

    Prelude> :info (.)
    (.) :: (b -> c) -> (a -> b) -> a -> c   -- Defined in ‘GHC.Base’
    infixr 9 .
    Prelude> :info (>>=)
    class Applicative m => Monad (m :: * -> *) where
      (>>=) :: m a -> (a -> m b) -> m b
      ...
        -- Defined in ‘GHC.Base’
    infixl 1 >>=
    

    (.) 具有更高的优先级(9 vs 1 用于&gt;&gt;=),因此它的参数将绑定得更紧密(即我们先将它们括起来)。但是我们怎么知道哪些是正确的呢?

    (mapM unpackNum params) >>= ((return . Number) . (foldl1 op))
    (mapM unpackNum params) >>= (return . (Number . (foldl1 op)))
    

    ...?因为(.) 被声明为infixr 它关联到右侧,这意味着上面的第二个解析是正确的。

    正如 Will Ness 在 cmets 中指出的那样,(.) 是关联的(例如加法),因此这两者在语义上恰好是等价的。

    只要有一点库(或本例中的 Prelude)的经验,您就可以学会正确地解析带有运算符的表达式,而无需考虑太多。

    如果在做完这个练习后你想了解一个函数的作用或它是如何工作的,那么你可以点击进入你感兴趣的函数的源代码并替换左-右手边和右手边(即内联函数和术语的主体)。显然,您可以在头脑中或在编辑器中执行此操作。

    【讨论】:

      【解决方案3】:

      您可以使用更适合初学者的语法,使用 do 表示法来“完善它”。您的函数 numericBinop op params = mapM unpackNum params &gt;&gt;= return . Number . foldl1 op 将变为:

      numericBinop op params = do
          x <- mapM unpackNum params -- "<-" translates to ">>=", the bind operator
          return . Number $ foldl1 op x
      

      现在最神秘的是mapM函数,也就是sequence . fmap,它简单地接受一个函数,fmaps它在容器上,然后翻转类型 em>,在这种情况下(我假设)从[Number Integer]ThrowsError [Integer],同时保留翻转过程中可能出现的任何错误(副作用),或者换句话说,如果“翻转”导致任何错误,它会在结果中表示。

      这不是最简单的例子,您最好看看mapM (fmap (+1)) [Just 2, Just 3]mapM (fmap (+1)) [Just 2, Nothing] 有何不同。如需更多见解,请查看Traversable typeclass。

      之后,您绑定 [Integer]ThrowsError monad 并将其提供给负责对列表进行计算的函数,从而产生单个 Integer ,而在将其包装成Number 后,您需要使用return 函数将其重新嵌入到ThrowsError monad 中。

      如果你仍然无法理解 monad,我建议你看看仍然相关的 LYAH chapter,它会轻轻地向你介绍 monad

      【讨论】:

      • 我很确定您脱糖的最后一行应该是“return $ Number $ foldl1 op x”
      • @PaulJohnson 数据构造函数,例如Number 只是函数。不明白为什么这是无效的。 return . Just 工作
      • 啊,是的。 “(return .Number)$ stuff”等价于“return $Number $ stuff”,(.) 具有更高的优先级。我现在明白了。
      【解决方案4】:

      &gt;&gt;= 构建了一个可能在任一端都失败的计算:它的左参数可以是一个空的 monad,在这种情况下它甚至不会发生,否则它的结果也可能是空的。它有类型

      >>= :: m a -> (a -> m b) -> m b
      

      看,它的参数是:浸入 monad 的值和接受纯值并返回浸入结果的函数。例如,该运算符是 Scala 中称为 flatMap 的一元版本;在 Haskell 中,它对列表的特殊实现称为concatMap。如果您有一个列表l,那么l &gt;&gt;= f 的工作方式如下:对于l 的每个元素,f 应用于此元素并返回一个列表;并将所有这些结果列表连接起来产生结果。

      考虑一段 Java 代码:

      try {
          function1();
          function2();
      }
      catch(Exception e) {
      }
      

      function2 被调用时会发生什么?看,在调用function1 之后,程序可能处于有效状态,所以function2() 是一个将当前状态转换为不同的下一个状态的运算符。但是对function1()的调用可能会导致抛出异常,所以控制会立即转移到catch-block——这可以认为是空状态,所以没有什么可以应用function2。换句话说,我们有以下可能的控制路径:

      [S0]  --- function1() -->  [S1]  --- function2() -->  [S2]
      [S0]  --- function1() -->  []    --> catch
      

      (为简单起见,从function2 抛出的异常不在此图中考虑。)

      所以,要么[S1] 是(非空)有效机器状态,function2 将其进一步转换为(非空)有效[S2],要么它是空的,因此function2() 是无操作,从不运行。这可以用伪代码概括为

      S2 <- [S0] >>= function1 >>= function2
      

      【讨论】:

        【解决方案5】:

        首先,语法。空格是应用程序,语义上:

        f x = f $ x  -- "call" f with an argument x
        

        所以你的表达其实是

        numericBinop op params = ((mapM unpackNum) params) >>= return . Number . (foldl1 op)
        

        接下来,运算符由非字母数字字符构建,没有任何空格。这里有.&gt;&gt;=。在 GHCi 上运行 :i (.):i (&gt;&gt;=) 会发现它们的 fixity 规范是 infixl 9 .infixr 1 &gt;&gt;=9 高于1 所以.&gt;&gt;= 强;因此

                               = ((mapM unpackNum) params) >>= (return . Number . (foldl1 op))
        

        infixl 9 . 表示. 关联到右边,因此,最后是

                               = ((mapM unpackNum) params) >>= (return . (Number . (foldl1 op)))
        

        (.) 定义为(f . g) x = f (g x),因此(f . (g . h)) x = f ((g . h) x) = f (g (h x)) = (f . g) (h x) = ((f . g) . h) x;通过 eta-reduction 我们有

        (f . (g . h)) = ((f . g) . h) 
        

        因此(.) 是关联的,因此括号是可选的。从现在开始,我们也将使用“空白”应用程序删除显式括号。因此我们有

        numericBinop op params = (mapM unpackNum params)  >>= 
                  (\ x -> return (Number (foldl1 op x)))   -- '\' is for '/\'ambda
        

        do写一元序列更容易,上面等价于

              = do 
                  {  x <- mapM unpackNum params        -- { ; } are optional, IF all 'do'
                  ;  return (Number (foldl1 op x)))    --   lines are indented at the same level
                  }
        

        接下来,mapM可以定义为

            mapM f [] = return []
            mapM f (x:xs) = do { x  <- f x ;
                                 xs <- mapM f xs ;
                                 return (x : xs) }
        

        单子定律要求

              do { r <- do { x ;       ===      do { x ;
                             y } ;                   r <- y ;  
                   foo r                             foo r 
                 }                                 }
        

        (您可以在我的this recent answer 中找到do 符号的概述);因此,

        numericBinop op [a, b, ..., z] =
           do {
                 a <- unpackNum a ;
                 b <- unpackNum b ;
                 ...........
                 z <- unpackNum z ;
                 return (Number (foldl1 op [a, b, ..., z]))
              }
        

        (您可能已经注意到我使用了x &lt;- x 绑定——我们可以&lt;- 的两侧使用相同的变量名,因为单子绑定不是 递归——因此引入了阴影。)

        希望现在更清楚了。

        但是,我说的是“首先,语法”。所以现在,它的意义。根据相同的单子定律,

        numericBinop op [a, b, ..., y, z] =
           do {
                 xs <- do { a <- unpackNum a ;
                            b <- unpackNum b ;
                            ...........
                            y <- unpackNum y ;
                            return [a, b, ..., y] } ;
                 z <- unpackNum z ;
                 return (Number (op (foldl1 op xs) z))
              }
        

        因此,我们只需要了解两个“计算”cd的顺序,

        do { a <- c ; b <- d ; return (foo a b) }
        =
                  c >>= (\ a ->
                           d >>= (\ b ->     
                               return (foo a b) ))
        

        对于涉及的特定 monad,由 bind (&gt;&gt;=) 运算符对给定 monad 的实现确定。

        Monad 是用于通用功能组合的 EDSL。计算的顺序不仅包括出现在do 序列中的显式表达式,还包括所讨论的特定 monad 特有的隐式效果,这些效果在幕后以原则和一致的方式执行。这就是首先拥有 monad 的全部意义(嗯,至少是要点之一)。

        这里涉及的 monad 似乎关心失败的可能性,以及在失败确实发生的情况下的早期救助。

        因此,使用do 代码,我们编写了我们打算发生的本质,并且对于我们来说,在幕后自动处理间歇性故障的可能性。

        换句话说,如果unpackNum 计算之一失败,那么整个组合计算 将失败,而不尝试任何后续unpackNum 子-计算。但如果它们都成功了,那么组合计算也会成功。

        【讨论】:

          猜你喜欢
          • 2021-08-15
          • 1970-01-01
          • 2018-04-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多