在应用解析中苦苦挣扎

时间:2014-12-16 11:35:07

标签: parsing haskell applicative

所以我正在编写一个复杂的解析器,只使用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}

现在让我们尝试使用applicative:

来编写它
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

Applicative已经强制我将变量绑定放在离实际解析器很远的位置。 (例如,尝试确认as实际上已绑定到sepBy argument ...;验证我没有错误计算_模式并不容易!)

另一个非常不直观的事情是<*>将函数应用于值,但*><*只是纯序列。这需要年龄来包裹我的思绪。不同的方法名称可以做得更远,更清晰。 (但遗憾的是,莫纳德似乎抓住了>><<。似乎这些可以叠加,产生像

这样的东西。
exit =
  "EXIT:" *>
  trim *>
  name <*
  char '(' <*
  trim <*
  char ')' <*
  trim

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

总而言之,我发现自己希望我可以使用do-notation!我实际上不需要根据先前的结果改变效果;我不需要 Monad的力量。但这种符号更具可读性。 (我一直想知道实现它是否真的可行;你能在语法上告诉我什么时候可以将特定的do-block机械转换为applicative?)

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

3 个答案:

答案 0 :(得分:9)

嗯,您的示例解析器人为复杂。

有很多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的版本更容易阅读。

回答更一般的问题:是的,可以识别do - 不需要monad力量的陈述。简单地说,它们是最后只有return的绑定序列的那些,并且所有绑定变量仅用于最终return而不是其他地方。将proposal添加到GHC中。 (就个人而言,我并不是它的忠实粉丝。我认为应用符号比写符号更有用。)

答案 1 :(得分:4)

编写较小的解析器。例如,您的参数似乎是(argument[, argument…])

可以很容易地表达出来
argListP :: Parser [Argument]
argListP = char '(' *> trim *> argument `sepBy` (char ',' *> trim) <* char ')'

仍然具有可读性:&#39;(&#39;后跟空格,参数用逗号和空格分隔,后跟&#39;)。您可以对result

执行相同操作
resultP :: Parser Result
resultP  = trim *> char '=' *> result

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

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

答案 2 :(得分:4)

我没有真正解决问题的方法,但也许一些直觉可能会帮助您更轻松地构建应用解析器。在应用方面,有两种&#34;排序&#34;这需要考虑:

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

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

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由多个部分组成,则必须使用括号:< / p>

-- 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)

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

应用符号,在某种意义上,强制您将解析器划分为逻辑块,以便更容易阅读,而不是可以想象地执行所有操作的monadic符号在一个整体的街区。