如何避免过多的样板用于匹配构造函数

时间:2017-02-27 09:32:18

标签: parsing haskell

我目前正在实施Lexer / Parser。有一件事让我感到困惑的是,目前我Parser.hs中的一半代码将专门用于获取单个令牌:

对于像这样的小数据类型:

data Tok
    = IdLower String 
    | IdUpper String 
    | IdSymbol String
    | IdColon String
    | Equals
    | Newline

我似乎需要这样的东西:

idLower :: Parser String
idLower = get >>= \s -> if
    | (_, IdLower n) :- xs <- s -> put xs *> pure n
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

idUpper :: Parser String
idUpper = get >>= \s -> if
    | (_, IdUpper n) :- xs <- s -> put xs *> pure n
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

idSymbol :: Parser String
idSymbol = get >>= \s -> if
    | (_, IdSymbol n) :- xs <- s -> put xs *> pure n
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

idColon :: Parser String
idColon = get >>= \s -> if
    | (_, IdColon n) :- xs <- s -> put xs *> pure n
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

equals :: Parser ()
equals = get >>= \s -> if
    | (_, Equals) :- xs <- s -> put xs
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

newline :: Parser ()
newline = get >>= \s -> if
    | (_, Newline) :- xs <- s -> put xs
    | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
    | Nil l <- s -> throwError [(l, "Unexpected end of input")]

这就像99%的重复代码,它们之间唯一的区别就是使用的构造函数,以及我是否有类似pure n的内容。

我尝试过重构一下,以便每个符号只有一个Tok -> Maybe ()Tok -> Maybe String函数,然后创建一个更高阶函数,将这些函数作为参数。但是每个Tok -> Maybe a函数需要3行加上1行spacer,现在我需要另一个更高阶函数来支持它,如果我想要shorthands,那么我可以使用idLower而不是{{1然后我最终会得到尽可能多的代码,如果不是更多的话!

我真的希望有上述的替代品。现在我知道我可以通过创建一个自动失败的功能来减少一些重复,如果第一个后卫没有击中,它总是会调用我可以推迟的相关getToken idLower,但即便如此还是感觉很糟糕。

2 个答案:

答案 0 :(得分:5)

您可以使用棱镜(例如来自lens library)免费获取Tok -> Maybe ()Tok -> Maybe String函数“(通过模板Haskell)。

data Tok =
    IdLower String
  | IdUpper String
  | IdSymbol String
  | IdColon String
  | Equals
  | Newline

makePrisms ''Tok

现在你可以说:

GHCi> preview _IdLower (IdLower "foo")
Just "foo"
GHCi> preview _IdLower (IdUpper "Foo")
Nothing

然后,正如您自己建议的那样,您可以在特定于令牌的函数中从棱镜中抽象出来:

tok :: Prism' Tok a -> Parser a
tok p = get >>= \ s -> if
  | (_, t) :- xs <- s, Just n <- preview p t -> put xs *> pure n
  | (l, t) :- _ <- s -> throwError [(l, "Unexpected " <> description t)]
  | Nil l <- s -> throwError [(l, "Unexpected end of input")]

然后,您可以通过说tok _IdLowertok _Equals来恢复单个功能。

答案 1 :(得分:2)

这是一种不同的方法,也需要一些样板代码,但没有模板Haskell。

它基于重构您的Milk was selected! Bread was selected! >> Script Ended 类型,以便您可以使用一种形式的相等测试而不是匹配,并且可以统一提取令牌的有效负载:

Tok

每个令牌都有令牌类型和有效负载。令牌类型是GADT 确定有效载荷的类型:

data Tok where
  Tok :: TokKind a -> a -> Tok

我们现在需要一种相等的形式,这意味着如果两个令牌具有相同的类型,则它们的有效载荷类型必须是兼容的。这是来自data TokKind :: * -> * where IdLower :: TokKind String IdUpper :: TokKind String IdSymbol :: TokKind String IdColon :: TokKind String Equal :: TokKind () Newline :: TokKind () 的{​​{1}}(遗憾的是,目前无法以任何简单的方式导出,除非再次使用TH):

testEquality

然后您的参数化标记函数变为

Data.Type.Equality