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