理解秒差距类型注解

Understanding parsec type annotations

我正在使用 parsec to parse some source code into an AST. I recently turned on the -Wall and -W options in order to catch "suspicious" code, and it is complaining about many of the parsec-related top-level functions in this file 没有明确的类型扩展。

示例 1

vimL = choice [ block
              , statement
              ]

这里推断的类型是:

vimL :: ParsecT String () Data.Functor.Identity.Identity Node

因此,如果我添加该注释,编译器会抱怨无法访问 Data.Functor.Identity.Identity,这意味着我必须 import 它:

import Data.Functor.Identity

如果我这样做,我可以将类型注释简化为:

vimL :: ParsecT String () Identity Node

并且编译器仍然会接受它。不过我还不是很了解。

示例 2

link = Link <$> (bar *> linkText <* bar)
  where
    bar      = char '|'
    linkText = many1 $ noneOf " \t\n|"

这里推断的类型是:

link :: forall u.
         ParsecT String u Data.Functor.Identity.Identity Node

但我不能使用它,除非我也使用:

{-# LANGUAGE RankNTypes #-}

请注意,如果我删除 forall,我可以免除它。这两项工作:

link :: ParsecT String u Data.Functor.Identity.Identity Node
link :: ParsecT String u Identity Node

示例 3

string' s = mapM_ char' s >> pure s <?> s

这个推断的类型是:

string' :: forall s u (m :: * -> *).
            Stream s m Char =>
             [Char] -> ParsecT s u m [Char]

为了使用那个,我需要两个:

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE KindSignatures #-}

但是,如果我删除 forall,我可以将类型简化为以下,编译器仍然接受它:

string' :: Stream s m Char => [Char] -> ParsecT s u m [Char]

虽然不是很简单。更进一步并删除约束:

string' :: [Char] -> ParsecT s u m [Char]

我得到:

No instance for (Stream s m Char) arising from a use of ‘char'’

我认为:

{-# LANGUAGE NoMonomorphismRestriction #-}

可能会让我摆脱困境,但事实并非如此。

问题

这些大多超出了我的理解,所以我不想在没有先获得更多洞察力的情况下盲目地复制粘贴推断的类型签名。任何人都可以阐明这些意味着什么,注释 parsec-heavy 代码的最佳实践是什么,如果 forall 可以省略而不会导致任何编译器错误,它给我带来了什么,以及是否有任何别名我可以使用哪些技巧来提高这些内容的可读性?

我不是 parsec 方面的专家,所以如果其他人想要解释这些类型,我会让他们做繁重的工作,但这里有一些想法:

通常包会尝试导出更友好的类型同义词。在这种情况下,您可以使用

type Parsec s u = ParsecT s u Identity -- in Text.Parsec.Prim
type Parser = Parsec String ()         -- in Text.Parsec.String

所以你 vimL :: Parser Node,这应该更有意义 - 它是一个解析器,可以 运行 在 String 上生成 Node

forall 在这种情况下得到 you 很少,这就是为什么有友好的类型同义词可用,你应该使用它们。但是,我敢打赌 parsec 本身就大量使用了更高级别的类型,如果没有 forall 就无法表达,这就是为什么 GHC 向您建议的签名有一个显式 forall.

(简而言之,forall x. <something-with-x><some-thing-with-x> 相同,但如果签名中间有 forall,事情会变得更糟。)

编辑

parsec 上的一些内容(from the documentation). The type ParsecT s u m a represents the most general parser possible. Reading the comments in the source 有帮助。

  • s描述流类型。抽象意义上的解析器采用一系列符号并将它们转换为某种结构化输出形式。
  • a是输出形式的类型。
  • u是用户状态类型。 parsec 已经跟踪了一些状态信息(比如你在文本中的位置,这样它可以给你返回一个有意义的解析错误消息)所以让用户在他们想要携带的某种状态下打包是有意义的(在 2.12 Advanced: User State)
  • 中有一个这样的例子
  • m 是底层 monad,事物在其中 运行。我认为这部分将是显而易见的,如果你 grok monads...

然后,出现了几个特殊情况:

  • 采用m = Identity意味着我们不需要单子执行上下文。 (Parsec s u a 类型同义词适用于这种情况。)
  • u = ()表示我们不需要持有任何状态信息
  • s = String意味着我们的输入(流)将是一个字符串。 (结合上面的其他两个选项,这就是 Parser a 类型同义词的用途。)

最后,string' :: forall s u (m :: * -> *). Stream s m Char => [Char] -> ParsecT s u m [Char] 意味着输出是 String = [Char],用户状态、单子上下文和输入可以是任何东西——只要它们满足某些条件,因此 Stream s m Char约束。

该约束 Stream s m t 意味着您必须能够 "unfold" 将流输入类型 s 转换为 m (Maybe (t,s))m 部分意味着这种展开可以发生在单子上下文中,Maybe 部分处理这样一个事实,即只要您有输入就可以展开,t 表示您正在离开流的前面,s 是流的其余部分。最后,stream的类型s要唯一标识出来的token的类型t,所以存在函数依赖s -> t.