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