Mat*_*hid 6 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}
Run Code Online (Sandbox Code Playgroud)
现在让我们尝试使用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
Run Code Online (Sandbox Code Playgroud)
Applicative 迫使我把变量绑定放在离实际解析器很远的地方.(例如,尝试确认这as
实际上是绑定的sepBy argument ...
;验证我没有错误的_
模式数量并不容易!)
另外一个很直观的是,<*>
应用一个函数的值,但*>
与<*
只是纯粹的测序.这花了年龄到绕到我的脑海里.不同的方法名称可以做得更远,更清晰.(但单子似乎已经抓住>>
和<<
可悲的.)看来,这些可堆叠,就像屈服的东西
exit =
"EXIT:" *>
trim *>
name <*
char '(' <*
trim <*
char ')' <*
trim
Run Code Online (Sandbox Code Playgroud)
你可以做到这一点是非常明显的.而且,对我来说,这段代码真的不是非常易读.更重要的是,我仍然没有弄清楚如何在删除多个其他值时处理多个值.
总之,我发现自己希望我可以使用do-notation!我实际上不需要根据先前的结果改变效果; 我并不需要单子的力量.但这种符号更具可读性.(我一直想知道实现它是否真的可行;你能在语法上告诉我什么时候可以将特定的do-block机械转换为applicative?)
有没有人知道解决这些问题的方法?最特别的是,如何将变量绑定移动到更接近它们绑定的解析器?
好吧,你的示例解析器是人为复杂的.
有很多trim
可以从中抽象出来:
token p = p <* trim
Run Code Online (Sandbox Code Playgroud)
您还可以从一对匹配括号之间的某些内容中抽象出来:
parens p = token (char '(') *> p <* token (char ')')
Run Code Online (Sandbox Code Playgroud)
现在剩下的是:
call =
(\ n as _ r -> Call {name = n, args = as, result = r}) <$>
name <*>
parens (sepBy argument (() <$ token (char ','))) <*>
token (char '=') <*>
result
Run Code Online (Sandbox Code Playgroud)
最后,你不应该计算出现_
,而是学会使用<$
和<*
.以下是有用的经验法则:
仅使用*>
在组合foo *> p <* bar
如parens
上述,别的地方.
让你的解析器有形式f <$> p1 <*> ... <*> pn
,而现在之间进行选择<$>
,并<$
在第一位置之间或<*>
与<*
所有其它位置为纯粹的你是否有兴趣在随后的解析器的结果的问题.如果是,请使用变体>
,否则,使用不带变体.然后你永远不需要忽略任何参数f
,因为你甚至无法访问它们.在上面的简化示例中,只=
留下我们不感兴趣的令牌,所以我们可以说
call = Call <$> name
<*> parens (sepBy argument (() <$ token (char ',')))
<* token (char '=')
<*> result
Run Code Online (Sandbox Code Playgroud)(这假设Call
实际上只需要这三个参数.)我认为这个版本比你原来do
的版本更容易阅读.
回答你更普遍的问题:是的,可以识别do
不需要monad功能的语句.简单地说,它们只是最后一个带有a的绑定序列return
,并且所有绑定变量仅用于最终return
和其他地方.有人建议将此添加到GHC.(就个人而言,我并不是它的忠实粉丝.我认为应用符号比功能符号更有用.)
我确实没有解决该问题的方法,但也许一些直觉可以帮助您更轻松地构建应用解析器。当谈到应用时,有两种“排序”需要考虑:
\n\n当两个序列彼此匹配良好时,结果是解析器在应用符号中的非常漂亮和紧凑的表示。例如:
\n\ndata Infix = Infix Double Operator Double\ninfix = Infix <$> number <*> operator <*> number\n
Run Code Online (Sandbox Code Playgroud)\n\n问题是,当序列不完全匹配时,您必须调整基础值才能使事情正常工作(您无法更改解析器的顺序):
\n\nnumber = f <$> sign <*> decimal <*> exponent\n where f sign decimal exponent = sign * decimal * 10 ^^ exponent\n
Run Code Online (Sandbox Code Playgroud)\n\n在这里,为了计算数字,您必须执行一些不平凡的操作组合,这是由局部函数 完成的f
。
另一种典型情况是您需要丢弃一些值:
\n\nexponent = oneOf "eE" *> integer\n
Run Code Online (Sandbox Code Playgroud)\n\n这里,*>
丢弃左侧的值,保留右侧的值。操作<*
员做相反的事情,丢弃右侧并保留左侧。当你有一系列这样的操作时,你必须使用左关联性来解码它们:
p1 *> p2 <* p3 *> p4 <* p5 \xe2\x89\xa1 (((p1 *> p2) <* p3) *> p4) <* p5\n
Run Code Online (Sandbox Code Playgroud)\n\n这是人为设计的:您通常不想这样做。最好将表达式分解为有意义的部分(最好给出有意义的名称)。您将看到的一种常见模式是:
\n\n-- discard the result of everything except `p3`\np1 *> p2 *> p3 <* p4 <* p5\n
Run Code Online (Sandbox Code Playgroud)\n\n但有一个小警告,如果您想应用其他内容p3
或p3
由多个部分组成,则必须使用括号:
-- applying a pure function\nf <$> (p1 *> p2 *> p3 <* p4 <* p5) \xe2\x89\xa1 p1 *> p2 *> (f <$> p3) <* p4 <* p5\n\n-- p3 consists of multiple parts\np1 *> p2 *> (p3\' <*> p3\'\') <* p4 <* p5)\n
Run Code Online (Sandbox Code Playgroud)\n\n同样,在这些情况下,通常最好将表达式分解为带有名称的有意义的片段。
\n\n从某种意义上说,应用表示法迫使您将解析器划分为逻辑块,以便更容易阅读,而不是一元表示法,您可以想象在一个整体块中完成所有操作。
\n