【问题标题】:Refactoring do notation into applicative style将 do 表示法重构为应用风格
【发布时间】:2014-08-13 18:56:25
【问题描述】:

所以我一直在用 Haskell 开发一个简单的表达式求解器。我一直在尝试将我的一些代码从 do 表示法重构为应用程序代码,主要是因为我想了解应用程序是如何工作的。我不知道如何反应这个

factor :: Parser Expr
factor = do
    char '('
    x <- buildExpr
    char ')'
    return x
<|> number
<|> variables
<?> "simple expression"

怎样才能使它成为一种应用风格?我尝试了以下方法,但它不会输入检查

factor = pure buildExpr <$> (char '(' *> buildExpr *> char ')')

其中 buildExper 的类型为 Parser Expr。

【问题讨论】:

  • 检查char '(' *&gt; buildExpr &lt;* char ')'的类型(注意最后一个*&gt;必须是&lt;*,从那里开始。

标签: haskell


【解决方案1】:

简答:

factor = (char '(' *> buildExpr <* char ')') <|> number <|> variables
     <?> "simple expression"

长答案:

&lt;$&gt; 有这种类型:

(<$>) :: (Functor f) => (a -> b) -> f a -> f b

换句话说,它接受一个函数和一个类型的值,该类型是Functor 的实例(并返回我们目前不关心的东西)。不幸的是,你没有给它一个函数作为第一个参数。你给它pure buildExpr,这是一个Parser,执行时不消耗任何输入并产生buildExpr。如果你真的想这样做,你可以使用&lt;*&gt;

factor = pure buildExpr <$> (char '(' *> buildExpr *> char ')')

这将运行pure buildExpr,从中提取函数,然后在(char '(' *&gt; buildExpr *&gt; char ')') 的结果上运行它。但不幸的是,我们也不能这样做:buildExpr 是某种Parser,而不是函数。

如果你想够了,你的脑海里应该会出现这样的想法:如果我们只想解析一个,为什么要提到两次buildExpr?事实证明,只提及一次就足够了。事实上,这可能几乎达到你想要的效果:

factor = char '(' *> buildExpr *> char ')'

唯一的问题是,它将产生Char ),而不是buildExpr 的结果。该死!但是查看文档并匹配类型,您最终应该能够发现,如果您将第二个 *&gt; 替换为 &lt;*,一切都会如您所愿:

factor = char '(' *> buildExpr <* char ')'

一个很好的助记符是箭头指向你想要保留的值。在这里,我们不关心括号,所以箭头指向外;但我们确实想保留buildExpr 的结果,所以箭头指向它的内部。

【讨论】:

  • 非常感谢。我的逻辑有几个漏洞,你的解释让我意识到了这一点。我刚刚开始理解应用程序并且一直坚持理解 的实际作用。我找不到足够的例子。你的解释很完美。
  • 你不会学到任何关于应用程序的想法,但在这种情况下你可以使用between
【解决方案2】:

所有这些运算符都是左结合的; &lt; 和/或 &gt; 指向贡献价值的事物; $ 表示 thing-to-left-is-pure-value 和 * 表示 thing-to-left-is-applicative-computation。

我使用这些运算符的经验法则如下。首先,列出语法产生的组成部分,并根据它们是否提供语义上重要的信息,将它们分类为“信号”或“噪音”。在这里,我们有

char '('      -- noise
buildExpr     -- signal
char ')'      -- noise

接下来,弄清楚“语义函数”是什么,它获取信号分量的值并给出整个生产的值。在这里,我们有

id     -- pure semantic function, then a bunch of component parsers
       char '('      -- noise
       buildExpr     -- signal
       char ')'      -- noise

现在,每个组件解析器都需要使用运算符附加到它之前的内容,但是哪个?

  • 始终以&lt; 开头
  • 下一个$ 用于第一个组件(就像之前的纯函数一样),或者* 用于其他所有组件
  • 如果组件是信号,则为&gt;,如果是噪声,则为

这给了我们

id     -- pure semantic function, then a bunch of parsers
   <$  char '('      -- first, noise
   <*> buildExpr     -- later, signal
   <*  char ')'      -- later, noise

如果语义函数是id,就像这里一样,你可以去掉它并使用*&gt;将噪声粘到id的参数信号的前面。我通常选择不这样做,只是为了让我可以清楚地看到语义功能位于制作的开头。此外,您可以通过穿插&lt;|&gt; 在这些产生式之间进行选择,并且您不需要将它们中的任何一个括在括号中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 1970-01-01
    • 1970-01-01
    • 2016-05-01
    • 2013-01-20
    • 1970-01-01
    • 2011-12-19
    相关资源
    最近更新 更多