【问题标题】:Understanding the attoparsec implementation (part 2)了解 attoparsec 实现(第 2 部分)
【发布时间】:2026-02-16 10:20:06
【问题描述】:

我目前正在努力学习和理解attoparsec库的源代码,但有些细节我自己也搞不清楚。比如Parser类型的定义:

newtype Parser i a = Parser {
      runParser :: forall r.
                   State i -> Pos -> More
                -> Failure i (State i)   r
                -> Success i (State i) a r
                -> IResult i r
    }

newtype Pos = Pos { fromPos :: Int }
            deriving (Eq, Ord, Show, Num)

data IResult i r =
    Fail i [String] String
  | Partial (i -> IResult i r)
  | Done i r

type Failure i t   r = t -> Pos -> More -> [String] -> String
                       -> IResult i r
type Success i t a r = t -> Pos -> More -> a -> IResult i r

我还没有完全理解的是类型参数r 的用法。如果我像这样定义runParser 的类型签名会有什么不同:

State i -> Pos -> More -> Failure i (State i) a -> Success i (State i) a a -> IResult i a

?

您能否帮我理解一下forall r. 在这种情况下的确切含义以及为什么必须在runParser 的类型签名中使用它?

提前多谢!

更新:进一步澄清我的问题:我目前不明白的是为什么必须首先引入类型参数r。可以想象,Parser 类型也可以这样定义:

newtype Parser i a = Parser {
      runParser ::
                   State i -> Pos -> More
                -> Failure i (State i) a
                -> Success i (State i) a
                -> IResult i a
}

data IResult i a =
    Fail i [String] String
    | Partial (i -> IResult i a)
    | Done i a

type Failure i t a  = t -> Pos -> More -> [String] -> String
                      -> IResult i a
type Success i t a = t -> Pos -> More -> a -> IResult i a

类型参数r 根本没有使用。我的问题是为什么这个定义是“错误的”以及它会带来什么问题......

【问题讨论】:

    标签: haskell attoparsec


    【解决方案1】:

    attoparsec 创建延续传递样式 (CPS) 解析器 如果没有forall,我们将无法链接 解析器在一起。

    这是一个大大简化的版本 涉及的类型和bindP 的定义 - 单子绑定运算符。 我们已经消除了故障延续和输入源。

    {-# LANGUAGE Rank2Types #-}
    
    type IResult r = r
    type Success a r = a -> IResult r  -- Success a r == a -> r
    
    newtype Parser a = Parser {
          runParser :: forall r. Success a r
                    -> IResult r
        }
    
    bindP :: Parser a -> (a -> Parser b) -> Parser b
    bindP m g =
        Parser $ \ks -> runParser m $ \a -> runParser (g a) ks
                                                      -----
                                                      -----
    

    注意Success a r 只是函数类型a -> r

    如果我们将runParser的定义替换为:

    runParser :: Success a a -> IResult a
    

    我们将在上面带下划线的位置收到类型错误。 要理解这一点,可以得出以下类型:

    ks                                      :: Success b b
    runParser m $ \a -> runParser (g a) ks  :: IResult b
    \a -> runParser (g a) ks                :: Success b b  == b -> b
    a :: b
    

    但从表达式(g a) 我们也可以得出结论a 具有a 类型 这给了我们类型错误。

    基本上Parser a 可以被认为是一种方法(或计算) 生成arunParser p ks 类型的值的方法是 获取该值并将其提供给采用a 的函数。 延续函数 ks 可以具有类型 a -> r 用于任何 r - 唯一的要求是它的输入类型是a。 通过在定义runParser 中使用Success a a,我们限制了 runParsera -> a 类型函数的适用性。这就是为什么我们 想将runParser 定义为:

    runParser :: Parser a -> (a -> r) -> r
    

    这种 CPS 风格的解析方法与所呈现的方法截然不同 在Monadic Parsing in Haskell

    【讨论】:

    • 就是这样!感谢您的完美解释!
    【解决方案2】:

    forall r 部分意味着此类型适用于所有r 作为您的结果,无需在newtype 声明的左侧指定。如Haskell wikibook中所述:

    forall 关键字用于显式地将类型变量引入作用域。

    r 类型变量在使用 forall 之前不在作用域内。那个wikibook页面用几个例子很好地解释了事情,所以我鼓励你看那里,但文章的关键是forall就像类型的交集,所以如果你把类型看作元素的集合, Bool = {False, True, ⊥}Int = {minBound..maxBound, ⊥}Char = {minBound..maxBound, ⊥}等(适当扩展minBound..maxBound表示未定义,通常称为底部),然后

    forall a. a
    

    是所有类型的交集,即{⊥},而

    forall a. Show a => a
    

    是受Show 约束的所有类型的交集。在这种情况下,当你有一个像

    data T = forall a. MkT a
    

    那么构造函数MkT的类型为forall a. a -> T,允许你将任何类型转换为T,这个技巧可以让你拥有异构列表:

    [MkT (), MkT 1, MkT "hello"] :: [T]
    

    但是你不能用这种类型做很多事情,你会如何打开MkT 来做任何事情呢?你可以在构造函数上进行模式匹配,但是你只会有一个 a 类型的值,而没有关于它的信息。如果你有

    data S = forall s. Show s => MkS s
    

    然后你可以做类似的事情

    -- Pass through instance of Show
    instance Show S where show (MkS s) = show s
    
    map show [MkS "hello", MkS 5, MkS (Just 2)]
    ["\"hello\"", "5", "Just 2"]
    

    就 attoparsec 而言,它与ST monad 有点相似,它使用forall 来防止您编写非法操作。同样,该 wikibooks 文章提供了一个很好的解释。如果还不够,请发表评论,我会看看我是否可以澄清。

    【讨论】:

    • 感谢答案和 wikibook 链接。而且我也更新了我的问题,所以如果你有时间,你能不能再看一遍,给我一些例子,为什么省略类型参数r会有问题?
    • @bmk 我不确定我是否有足够的知识来详细说明 attoparsec 中的设计选择。您可以尝试进行更改,然后用它重新实现 attoparsec 的一小部分,看看有什么区别(如果我想自己弄清楚我的行动方案是什么),或者您可以尝试给维护者发电子邮件,谁可能能够非常轻松快速地向您解释。