【问题标题】:Struggling with Applicative parsing在应用解析中苦苦挣扎
【发布时间】:2014-12-16 11:35:07
【问题描述】:

所以我正在尝试编写一个复杂的解析器,只使用 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 ...;要验证我没有得到错误的_ 模式计数并不容易!)

另一个非常不直观的事情是&lt;*&gt; 将函数应用于一个值,但*&gt;&lt;* 只是纯粹的排序。这花了我 ages 的时间。不同的方法名称会使这一点更加清晰。 (但遗憾的是,Monad 似乎抓住了&gt;&gt;&lt;&lt;。)似乎这些可以堆叠,产生类似

exit =
  "EXIT:" *>
  trim *>
  name <*
  char '(' <*
  trim <*
  char ')' <*
  trim

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

总之,我发现自己希望我可以使用 do-notation!我实际上不需要根据先前的结果来改变效果;我不需要 Monad 的力量。但是这个符号更具可读性。 (我一直想知道实现这一点是否真的可行;你能从语法上判断一个特定的 do-block 何时可以机械地转换为 applicative 吗?)

有人知道解决这些问题的方法吗?尤其是,我怎样才能将变量绑定更靠近它们绑定的解析器?

【问题讨论】:

  • 我想等待Applicative-Do 对某些人来说太难了;)
  • 我不认为&lt;&lt; 存在,至少在base 中不存在。
  • @BartekBanachewicz Applicative-do... 功能请求已经存在。我想这是真的;如果你能想到的话,它已经在互联网的某个地方了。
  • 这些都是很好的答案。我什至不确定选择哪一个作为“正确”答案...
  • 来自未来的你好,ApplicativeDo已登陆。

标签: parsing haskell applicative


【解决方案1】:

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

有很多 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

最后,您不应该计算_ 的出现次数,而应该学习使用&lt;$&lt;*。以下是有用的经验法则:

  • 仅在foo *&gt; p &lt;* bar 组合中使用*&gt;,例如上面的parens,别处。

  • 让你的解析器有f &lt;$&gt; p1 &lt;*&gt; ... &lt;*&gt; pn的形式,现在在&lt;$&gt;&lt;$之间选择第一个位置或&lt;*&gt;&lt;*之间的所有其他位置纯粹是关于你是否是对后续解析器的结果感兴趣。如果是,请使用带有&gt; 的变体,否则,请使用不带的变体。然后你永远不需要忽略f 中的任何参数,因为你甚至无法访问它们。在上面的简化示例中,只剩下我们不感兴趣的= 标记,所以我们可以说

    call = Call <$> name
                <*> parens (sepBy argument (() <$ token (char ',')))
                <*  token (char '=')
                <*> result
    

(这是假设 Call 实际上只接受这三个参数。)我认为这个版本甚至比基于 do 的原始版本更容易阅读。

回答您更一般的问题:是的,可以识别不需要单子功能的do-statements。简单地说,它们只是最后一个带有return 的绑定序列,并且所有绑定变量仅在最终的return 中使用,其他任何地方都没有。有一个proposal 可以将此添加到 GHC。 (但是,就我个人而言,我并不是它的忠实拥护者。我认为 applicative notation 比 do-notation 更实用。)

【讨论】:

    【解决方案2】:

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

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

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

    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
    

    这里,*&gt; 丢弃左边的值,保留右边的值。 &lt;* 运算符则相反,丢弃右边并保留左边。当您有一系列此类操作时,您必须使用左关联性对其进行解码:

    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)
    

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

    应用符号,在某种意义上,迫使你将解析器分成逻辑块,以便更容易阅读,而不是一元符号,你可以想象在一个整体块中完成所有事情.

    【讨论】:

    • 问题实际上是我试图解析没有“逻辑结构”的“人类可读”文本。它只是一个随机纠结的子句,很难命名。不过,我认为最后一段真的说明了一切。
    • 你在做自然语言处理吗?
    • 更像是解析另一个程序的输出。但是输出不是具有已定义语法的代码;这只是人类直觉的东西。所以这是相当不一致的......
    【解决方案3】:

    编写更小的解析器。例如,您的论点似乎是(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
    

    【讨论】:

      猜你喜欢
      • 2021-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-21
      相关资源
      最近更新 更多