您如何在 Haskell 中建模 "metadata"?

How do you model "metadata" in Haskell?

我正在 Haskell 中编写解析器(主要是为了学习)。我有一个可用的分词器和解析器,我想在给出错误消息时添加行号。我有这种类型:

data Token = Lambda
  | Dot
  | LParen
  | RParen
  | Ident String

回到 OO 领域,我将只创建一个元数据对象来保存令牌在源代码中的位置。所以我可以试试这个:

data Metadata = Pos String Int Int

然后,我可以将 Token 更改为

data Token = Lambda Metadata
  | Dot Metadata
  | LParen Metadata
  | RParen Metadata
  | Ident String Metadata

但是,我的解析器是使用标记上的模式匹配编写的。所以现在,我所有的模式匹配都被破坏了,因为我还需要考虑元数据。所以这似乎并不理想。 99% 的时间,我不关心元数据。

那么做我想做的事情的“正确”方法是什么?

Haskell 中有多种设计语法表示的方法,但我可以提供一些建议和推理。

建议将 Token 类型的元数据注释 排除在 之外,以便它坚持单一职责。如果 Token 表示 只是 一个标记,它的 Eq 等的派生实例将按预期工作,而无需担心何时忽略注释。

谢天谢地,在这种情况下,备选方案很简单。一种选择是将注释信息移动到单独的包装器类型。

-- An @'Anno' a@ is a value of type @a@ annotated with some 'Metadata'.
data Anno a = Anno { annotation :: Metadata, item :: a }
  deriving
    ( Eq
    , Ord
    , Show
    -- …
    )

现在标记器可以 return 一系列带注释的标记,即 [Annotated Token]。您仍然需要更新使用站点,但更改现在要简单得多。您可以通过多种方式忽略注释:

-- Positional matching
f1 (Anno _meta (Ident name)) = …

-- Record matching
f2 Anno { item = Ident name } = …

-- With ‘NamedFieldPuns’
f3 Anno { item } = …

-- 'U'nannotated value; with ‘PatternSynonyms’
pattern U :: a -> Anno a
pattern U x <- Anno _meta x

f4 (U LParen) = …

您可以使用 fmap item 取消注释一系列标记,以重用不关心位置信息的现有代码。而由于AnnoType -> Type的一种类型,GHC也可以为它派生出FoldableFunctorTraversable,方便对带注释的项目,例如fmaptraverse.

这是 Token 的首选方法,但对于包含注释的已解析 AST,您可能希望将注释类型作为 AST 类型的参数,例如:

data Expr a = Add a (Expr a) (Expr a) | Literal a Int
  deriving (Eq, Foldable, Functor, Ord, Show, Traversable)

然后您可以对带注释的术语使用 Expr Metadata,或对未注释的术语使用 Expr ()。要比较条款是否相等,例如在单元测试中,您可以使用 Functor 实例去除注释,例如void expr1 == void expr2,这里的void相当于fmap (\ _meta -> ())

在较大的代码库中,如果有很多代码取决于数据类型并且您真的想要避免一次更新所有代码,您可以将旧类型包装在为每个旧构造函数导出模式同义词的模块。这使您可以在删除适配器模块之前逐步更新旧代码。

在文化上,self-contained Haskell 代码库中的典型做法是简单地进行重大更改,然后让编译器告诉您需要更新的所有地方,因为使用高度保证它是正确的。当涉及到已发布的库代码时,我们更关心向后兼容性,因为这实际上会影响其他人。