在应用性解析方面苦苦挣扎



所以我正在尝试编写一个复杂的解析器,仅使用Applicative(有问题的解析器甚至根本没有实现Monad(。

对于琐碎的解析器,这很容易。对于不平凡的...没那么多。应用界面似乎猛烈地迫使您以无点风格编写所有内容。这是极难处理的。

例如,考虑:

call = do
  n <- name
  char '('
  trim
  as <- sepBy argument (char ',' >> trim)
  char ')'
  trim
  char '='
  r <- result
  return $ Call {name = n, args = as, result = r}

现在让我们尝试使用应用程序来编写它:

call =
  ( n _ _ as _ _ _ _ r -> Call {name = n, args = as, result = r}) <$>
  name <*>
  char '(' <*>
  trim <*>
  sepBy argument (const const () <$> char ',' <*> trim) <*>
  char ')' <*>
  trim <*>
  char '=' <*>
  trim <*>
  result

应用迫使我将变量绑定放在离实际解析器位置很远的地方。(例如,尝试确认as实际上绑定到sepBy argument ...;要验证我没有错误的_模式计数并不容易!

另一个非常不直观的事情是,<*>将函数应用于值,但*><*只是纯粹的排序。这花了很长时间才把我的思想包裹起来。不同的方法名称会使这一点更加清晰。(但莫纳德似乎抓住了>>,可悲的是<<。似乎这些可以堆叠,产生类似

exit =
  "EXIT:" *>
  trim *>
  name <*
  char '(' <*
  trim <*
  char ')' <*
  trim

你可以做到这一点是相当不明显的。而且,对我来说,这段代码真的不是很好读。更重要的是,我仍然没有弄清楚如何处理收集多个值,同时删除多个其他值。

总而言之,我发现自己希望我能使用do符号!我实际上不需要根据先前的结果更改效果;我不需要莫纳德的力量。但是符号的可读性要高得多。(我一直想知道实现这一点是否真的可行;你能在语法上判断一个特定的do块何时可以机械地转换为应用块吗?

有人知道解决这些问题的方法吗?最特别的是,如何将变量绑定移近它们绑定到的解析器?

好吧,你的示例解析器是人为复杂的

有很多

出现的trim可以从中抽象出来:

token p = p <* trim

您还可以从一对匹配括号之间发生的事件中抽象出来:

parens p = token (char '(') *> p <* token (char ')')

现在剩下的就是:

call =
  ( n as _ r -> Call {name = n, args = as, result = r}) <$>
  name <*>
  parens (sepBy argument (() <$ token (char ','))) <*>
  token (char '=') <*>
  result

最后,你不应该计算_的出现次数,而是应该学会使用<$<*。以下是有用的经验法则:

  • 仅在组合中使用*> foo *> p <* bar例如在上面的parens中,而不是其他地方。

  • 让你的解析器具有 f <$> p1 <*> ... <*> pn 的形式,现在在第一个位置的 <$><$ 之间做出选择,或者在所有其他位置的 <*><* 之间进行选择,纯粹取决于您是否对后续解析器的结果感兴趣。如果是,请使用带有>的变体,否则,请使用没有的变体。然后你永远不需要忽略f中的任何参数,因为你甚至无法访问它们。在上面的简化示例案例中,只剩下我们不感兴趣的=令牌,因此我们可以说

    call = Call <$> name
                <*> parens (sepBy argument (() <$ token (char ',')))
                <*  token (char '=')
                <*> result
    

(这是假设Call实际上只接受这三个参数。我认为这个版本甚至比你原来的基于do的版本更容易阅读。

回答你更普遍的问题:是的,可以识别不需要monads力量的do语句。简单地说,它们只是一系列最后带有return的绑定,所有绑定变量只在最后的return中使用,而不是在其他地方使用。有人提议将其添加到GHC中。(就个人而言,我不是它的忠实粉丝。我认为应用符号比do符号更实用。

我真的没有解决这个问题的方法,但也许一些直觉可以帮助你更轻松地构建应用解析器。 在应用方面,需要考虑两种"排序":

    解析
  • 操作的顺序:这决定了您编写解析器的顺序。
  • 基础值的排序:这个更灵活,因为您可以按您喜欢的任何顺序组合它们。

当两个序列相互匹配良好时,结果是解析器在应用符号中的非常漂亮和紧凑的表示。 例如:

data Infix = Infix     Double     Operator     Double
infix      = Infix <$> number <*> operator <*> number

问题是,当序列不完全匹配时,您必须调整基础值才能正常工作(您无法更改解析器的顺序(:

number = f <$> sign <*> decimal <*> exponent
  where f sign decimal exponent = sign * decimal * 10 ^^ exponent

在这里,为了计算数字,您必须执行稍微不平凡的操作组合,这是由本地函数 f 完成的。

另一种典型的情况是您需要丢弃一些值:

exponent = oneOf "eE" *> integer

在这里,*>丢弃左侧的值,请将值保留在右侧。 <*运算符则相反,丢弃右侧并保留左侧。 当你有一连串这样的操作时,你必须使用左关联性来解码它们:

p1 *> p2 <* p3 *> p4 <* p5  ≡  (((p1 *> p2) <* p3) *> p4) <* p5

这是人为的:你通常不想这样做。 最好将表达式分解为有意义的部分(最好给出有意义的名称(。 您将看到的一种常见模式是:

-- discard the result of everything except `p3`
p1 *> p2 *> p3 <* p4 <* p5

不过有一个小警告,如果你想将其他东西应用于p3或者如果p3由多个部分组成,你将不得不使用括号:

-- applying a pure function
f <$> (p1 *> p2 *> p3 <* p4 <* p5)  ≡  p1 *> p2 *> (f <$> p3) <* p4 <* p5
-- p3 consists of multiple parts
p1 *> p2 *> (p3' <*> p3'') <* p4 <* p5)

同样,在这些情况下,通常最好将表达式分解为带有名称的有意义的片段。

从某种意义上说,应用符号迫使您将解析器划分为逻辑块,以便于阅读,这与一元表示法相反,您可以在一个整体块中完成所有操作。

编写较小的解析器。例如,您的论点似乎很(argument[, argument…]).这可以很容易地表达为:

argListP :: Parser [Argument]
argListP = char '(' *> trim *> argument `sepBy` (char ',' *> trim) <* char ')'

这仍然非常可读:一个"("后跟空格,用逗号和空格分隔的参数,以及尾随的"("。也可以为您的result做同样的事情:

resultP :: Parser Result
resultP  = trim *> char '=' *> result

如您所见,这仍然是可读的:任意空格,后跟等号和某种结果。现在call几乎是微不足道的:

call :: Parser Call
call = Call <$> name <*> argListP <*> resultP

最新更新