【问题标题】:Parsing custom infix operators + implementation with FParsec使用 FParsec 解析自定义中缀运算符 + 实现
【发布时间】:2019-02-16 18:19:30
【问题描述】:

我有点卡在“真正的解析器”(例如 F# 或 Haskell)解析自定义运算符的方式上。对于“普通”语言,我们只需定义一个 AST 节点,在该节点处将有预定义的运算符可能性,例如:+-*==>=+=、 ...等等。

但我想知道如何用一种允许您创建自定义运算符的函数式语言来做到这一点,让我们以 OCaml 为例,非常接近 F#(我的实现语言),并且众所周知。

因此,每个运算符都是一个函数,有一个类型,还有一个定义,我们可以创建自己的运算符:

val (+) : 'a -> 'a -> 'a
let (+) x y = x + y

val (|>) : 'a -> ('a -> 'b) -> 'b
let (|>) x f = f x

所以我想知道它是如何与解析一起工作的。

1) 解析器如何知道我们要使用自定义运算符?如果我们使用的函数在第一个参数中接受另一个函数,在第二个参数中接受另一个元素,它怎么知道我们调用的是函数而不是使用中缀运算符?

let example x =
    // Do we add up, or do we call the function "takeOpAndOther"?
    takeOpAndOther + x

2) 为了回答这个问题,我想到了一种在 F# 中执行此操作的方法,这要感谢 FParsec。想到的第一个解决方案是简单地使用OperatorPrecedenceParser。令人担忧的是,这意味着只适用于预定义的运算符(或者如果有办法用它做我想做的事,我不知道该怎么做)。

然后我想到了创建一个简单的解析器:

open FParsec

type Expression =
    | Number of int
    | InfixF of Expression * string * Expression
    | DataName of string
    | FunctionCall of string * Expression list

let ws = skipMany (pchar ' ' <|> pchar '\t') <?> ""
let ws1 = skipMany1 (pchar ' ' <|> pchar '\t') <?> ""

let identifier = many1Satisfy (fun c -> isLetter c || isDigit c)

let allowedSymbols =
   [ '!'; '@'; '#'; '$'; '%'; '^'; '&';
     '§'; '*'; '°'; '.'; '~'; ':'; '-';
     '+'; '='; '?'; '/'; '>'; '<'; '|'; ]

let customOperatorIdentifier = many1SatisfyL (fun c -> allowedSymbols |> List.contains c) "valid custom operator"

// I call about this parser
let rec infixF () = parse {
        let! lvalue = ws >>? expression
        let! op = ws >>? customOperatorIdentifier
        let! rvalue = ws >>? expression
        return InfixF(lvalue, op, rvalue)
    }

and number = pint32 |>> Number

and dataName = identifier |>> DataName

and functionCall () = parse {
        let! id = ws >>? identifier
        let! parameters = sepEndBy1 (ws >>? expression) ws1
        return FunctionCall(id, parameters)
    }

and expression =
    attempt number <|>
    attempt dataName <|>
    attempt (functionCall ()) <|>
    infixF ()

let test code =
    match run (ws >>? expression .>>? ws .>>? eof) code with
    | Success (result, _, _) -> printfn "%A" result
    | Failure (msg, _, _)    -> printfn "%s" msg

test "87 + 12"

除非,正如您所料,它没有按预期工作。确实,当代码出现时(因为当我单独尝试infixF 并将其从expression 中删除时,它可以工作,但显然只适用于一个表达式:x + y,而不是x + y + z),它会导致每次溢出错误。我认为这是我在实现中遇到的主要问题。

但是,所描述的两种解决方案不能满足我的一个问题,即函数运算符的发送。

简而言之...我有一些问题想得到解释,以及一个我想解决的实现问题。

谢谢! :)

【问题讨论】:

  • 现在没有时间给出完整的答案,但您对OperatorPrecedenceParser 仅适用于预定义运算符的担忧似乎没有根据。有一个AddOperator method 可以动态添加新的操作符;因此,当您解析 F# 的 let (&gt;=&gt;) f g = ... 之类的内容时,您可以将 &gt;=&gt; 运算符动态添加到解析器中。我自己还没有做过,所以我不能就具体细节给你建议,但是一些实验应该会有所帮助。
  • 我很想知道该怎么做。在我看来,动态添加确实是可能的,但不是我想做的,这似乎设置起来稍微复杂一些。
  • @rmunn 但是这不适用于相互递归,例如let rec (~+) a b = a ~* (b - 1) and (~*) a b = if b&lt;0 then a else (a+b) ~+ b in ... 因为~* 是在使用后按词法定义的。
  • 堆栈溢出错误的原因是因为您的语法(表达式和中缀表达式)形成了左递归,您需要将其重构为右递归。 en.m.wikipedia.org/wiki/Left_recursion

标签: parsing f# operators ocaml fparsec


【解决方案1】:

所以你是对的,困难的部分是优先级。我认为对于 ML 风格的语言,大约有两种方法可以处理它

  1. 优先级由固定规则定义
  2. 优先级由用户定义

Ocaml 执行选项 1。运算符的优先级和关联性由其第一个字符定义。

Haskell 执行选项 2。优先级和关联性是用语句定义的(声明可以在使用运算符之后)。

查看如何解析 (1) 非常简单:您只需正常解析它,除了在该优先级仅允许运算符 + 之外,您定义任何以 + 开头的运算符。这就留下了你应该如何处理解析像a +* b +- c 这样的表达式的问题。我不知道 ocaml 将如何关联它,但我的猜测要么基于第二个字符,要么基于相同的优先级(例如,以相同的优先级解析 +- 并关联到左侧所以a + b - c + d 解析为 ((a + b) - c) + d)。

我认为您对解析 (2) 也有正确的想法,但这很棘手。我认为您的类型略有错误,您真正想要的是:

type operator = Op of string
type expression =
  | Var of string
  | Operator of operator
  | App of expression * expression
  | Tuple of expression list
  | Infix of expression * (operator * expression) list

特别是你不能有Infix of expression * operator * expression,因为那你如何解析a OP b OP c?你基本上有两种选择:

  1. Infix (Infix (Var a, Op OP, Var b), Op OP, Var c)
  2. Infix (Var a, Op OP, Infix (Var b, Op OP, Var c))

选项 1 等效于 (a OP b) OP c,适用于 -|&gt;,但不适用于 Haskell 样式 $,当然也不适用于 a + b * c。同样,选项 2 适用于 +,但不适用于 -/。此外,在排序优先级之前仅撤消此重整是不够的,因为表达式 (a OP b) OP c 必须被解析为选项 1,即使它未重整。

请注意,我们(如果我们想要一种 ML 风格的语言)需要一种将运算符的功能表示为值的方法,例如(+) 但这可以包含在 Var 中。

一旦你有了这个级别的解析,你就可以等到你确定了运算符的任何运算符优先级规则,然后你就可以解析了。

其他一些可能值得考虑的事情:

  1. 前置/后缀运算符:Ocaml 允许前缀运算符,前提是它们以特定符号开头,例如!。 Haskell 允许后缀运算符作为扩展,但仅使用切片(即扩展将 (x*) 的定义从 (\y -&gt; (*) x y) 放松为 ((*) x),因此 (*) 可以采用单个参数。如果您希望能够拥有 pre 和后缀运算符是用户定义的,您可以更改类型以删除应用程序以及表达式之间可以只有一个运算符的规则,然后有一个步骤将expression | operator 的列表解析为理智的东西,例如a * + b解析为a (*(+b))(a) * (+b)(a*) (+b)(a*) + (b)((a*)+) b?也许这种困难对人类读者也不利。
  2. 如何处理优先级?在 Haskell 中,您可以选择 0 到 9 之间的整数。在 perl6 中,您只需说例如 * 比 + 更紧,如果两个具有未定义关系的运算符一起出现,则该语言要求您放入括号。

也许值得注意的是 perl6 方式作为另一种选择。在这一个中,运算符必须在使用之前定义它们的优先级和关联性/固定性,并且解析器在声明和使用的它们之间动态添加这些(也可以使用语言的整个语法来做到这一点,以便解析未来的表达式依赖于评估早期的稍微不那么疯狂)。

【讨论】:

  • 好的,谢谢你的回答,我知道如何继续。我想你已经回答了我的问题 (1),以及如何在我的问题 (2) 中看到它。
  • @Foxy 不幸的是,我无法很好地解释为什么您的解析器会爆炸。
  • 没问题^^。我会看看其他成员是否有解决我的问题的解决方案,或者至少提出一种比我更好的做事方式。
猜你喜欢
  • 2015-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多