【问题标题】:Haskell do notation edge case fails to typecheckHaskell do notation 边缘情况无法进行类型检查
【发布时间】:2020-03-07 16:29:25
【问题描述】:

我正在尝试理解do 表示法的规则。

这是一些类型检查的代码:

fff :: Maybe Int
fff = do
    _ <- pure $ Just 100
    (+10)
    <$> Just 50

基本上是fff = (+10) &lt;$&gt; Just 50。我会假设上述内容可能会进行类型检查 - 因为肯定每一行都应该在 Maybe 的上下文中,而 (+10) 不是。

为什么要进行上述类型检查?下面是一个更简单的例子:

fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

为什么上面的语法被认为是有效的?这不是“脱糖”吗:

fff i = ((+10) >>= (\i -> fmap (Just i))) i

这确实在 ghci 中给出了类型检查错误。


这是一个示例,它按照与上述类似的缩进进行类型检查:

x :: Maybe Int
x = do
  _ <- Just 1
  undefined
  <$> Just 5

(感谢上述示例中来自 FP 闲聊的@cvlad)

【问题讨论】:

  • 您的问题不是关于 do 表示法,而是关于 GHC 解析器的内部工作原理。所以当你把它分成两行时它无法识别那个表达,那又怎样。不要在第二行缩进多于第一行的情况下这样做! :) 并且您没有包含错误消息。我希望这些错误会告诉你所涉及的子表达式,这样你就可以看到它是如何读取它们的。
  • @WillNess 这是个好问题。 为什么解析器/类型检查器接受一个而不接受另一个?
  • 显然第一个符合some规则;如果您不知道,则无需将问题视为不值得提出而忽略。
  • @Chris 你已经彻底改变了这个问题。您将 do 块解释为绑定代码不正确:&lt;$&gt;infix 二元运算符。

标签: haskell syntax do-notation


【解决方案1】:

这是一种奇怪的互动。

我开始将测试用例简化为这个,运行良好。

> x = do succ ; <$> Just 1
> x
Just 2

相比之下,这不会解析:

> y = do { succ ; <$> Just 1 }      
error: parse error

但是,这会解析:

> z = do { succ } <$> Just 1      
> z
Just 2

所以,这就是我认为正在发生的事情。由于令牌&lt;$&gt; 永远无法启动表达式,因此解析暂时失败。 do 解析器规则本质上是最大咀嚼规则:失败时,添加隐式 } 并重试。

正因为如此,上面的x被解析为z。由于succ 是一元值(OP 问题中的(+10) 行),它可以出现在do 中。这使得类型检查成功。

引用Haskell Report 2.7

只要句法类别也插入一个右大括号 包含布局列表结束;也就是说,如果一个非法的词位是 在一个闭合括号是合法的点遇到的,一个闭合的 大括号已插入。

【讨论】:

  • do 1do "1" 一样解析。所以它甚至不需要是一元值。
【解决方案2】:
fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

为什么上面的语法被认为是有效的?

因为它被解析为

fff i = do {        -- do { A } is just
    (+10) }         --      A
    <$> Just i

相当于

fff i =
    (+10) 
    <$> Just i

因为&lt;$&gt; Just i 本身是一个无效的表达式(所以fff i = ((+10) &gt;&gt;= (\i -&gt; fmap (Just i))) i 是不正确的翻译),并且根据@chi 的答案中引用的规则来界定do 块的范围。

确实它的类型被推断为

fff :: Num b => b -> Maybe b

如果您在最后一行的 &lt;$&gt; 之前添加一个空格,则您的第二个示例有效。没有空格,再次解析为

inputTest :: FormInput -> IO (Either [String] (Int, Int))
inputTest fi = do {
    allErrors' <- undefined :: IO [String]
    undefined }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

因为 &lt;$&gt; ... 本身就是无效的表达式。确实,当我添加显式分隔符时,

inputTest2 :: String -> IO (Either [String] (Int, Int))
inputTest2 fi = do {
    allErrors2 <- undefined :: IO [String] ;
    undefined  }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

我在 TIO 上得到了完全相同的错误消息(必须使用 String 而不是你的类型)。

从第一个 undefined :: IO [String] 开始,整个 do 块都有一些 IO t 类型,我们不能将 那个 映射到任何东西上。

始终添加所有显式分隔符(除了练习良好的缩进样式),以避免这种奇怪的语法脆弱性。


你的新例子是

x :: Maybe Int
x = do          -- {     this is
  _ <- Just 1   -- ;       how it is
  undefined     -- }         parsed
  <$> Just 5

代码改变了,但答案是一样的。 do之前 &lt;$&gt;Maybe t(因为Just 1),我们不能fmap 那个

再次,将最后一行再缩进一些,它会编译,因为undefined &lt;$&gt; Just 5 现在将被解析为一个表达式。

【讨论】:

    猜你喜欢
    • 2015-08-22
    • 1970-01-01
    • 2018-09-05
    • 2014-03-26
    • 2013-05-19
    • 1970-01-01
    • 2012-05-13
    • 2023-04-03
    • 1970-01-01
    相关资源
    最近更新 更多