所以我正在编写一个复杂的解析器,只使用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?)
有人知道解决这些问题的方法吗?最重要的是,如何将变量绑定移动到更接近它们绑定的解析器?
答案 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符号在一个整体的街区。