使用FParsec解析自定义中缀运算符+实现

时间:2019-02-16 18:19:30

标签: parsing f# operators ocaml fparsec

我对诸如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)解析器如何知道我们要使用自定义运算符?如果我们使用的函数在第一个参数中使用另一个函数,在第二个参数中使用另一个元素,那么它怎么知道我们调用函数而不是使用infix运算符?

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

2)为了回答这个问题,感谢FParsec,我想到了一种在F#中执行此操作的方法。我想到的第一个解决方案是简单地使用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,而不是{{ 1}}),每次都会导致溢出错误。我认为这是我在实现过程中遇到的主要问题。

但是,描述的两个解决方案不能满足我的问题之一,那就是函数操作符的发送。

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

谢谢! :)

1 个答案:

答案 0 :(得分:2)

所以您说对了,难点是优先。我认为对于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,适用于-|>,但不适用于Haskell样式$,当然不适用于a + b * c。同样,选项2适用于+,但不适用于-/。此外,仅在排序优先级之前撤消此重整是不够的,因为表达式(a OP b) OP c必须解析为选项1,即使它没有残缺。

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

一旦有了这种解析级别,就可以等待,直到为运算符确定了任何运算符优先级规则,然后就可以进行解析。

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

  1. 前缀/后缀运算符:Ocaml允许前缀运算符(前提是它们以特定符号开头),例如!。 Haskell允许将后缀运算符用作扩展名,但只能使用切片(即,扩展名将(x*)的定义从(\y -> (*) x y)松散到((*) x),因此(*)可以采用单个参数。您希望能够由用户定义前缀和后缀运算符,则可以更改类型以删除应用程序,并更改规则,即表达式之间可以只有一个运算符,然后有一个步骤可以解析{{1} }变成理智的事物,例如expression | operator解析为a * + ba (*(+b))(a) * (+b)(a*) (+b)(a*) + (b)吗?对人类读者也不利。
  2. 如何处理优先级?在Haskell中,选择0到9之间的一个整数。在perl6中,您只说例如*比+紧,并且如果两个具有不确定关系的运算符同时出现,则该语言要求您放入括号。

也许值得一提的是perl6方法。在这种情况下,运算符在使用之前必须先定义其优先级和关联性/固定性,并且解析器会动态地在声明和使用的操作符之间添加它们(一个人也可以使用语言的整个语法来做到这一点,以便解析将来的表达式依靠对早期评估进行评估就不会那么疯狂了。