F#解析器库FParsec实现公式求导【FParsec | 03】

     在之前的博客中,我已经介绍了一下如何用F#对数学表达式进行求导,但是其中有一个问题,就是求导的对象是一个Expr类型,这样使用起来是很不方便的。一般来说,用户使用都是直接输入一个文本类型的数学公式,然后求导系统应该就可以对其进行自动解析成Expr类型,并根据求导规则进行求导的符号计算。

    如果对之前撰写的博文【如何用F#对数学表达式进行求导运算】,感兴趣的话可以自行去阅读。下面,给出利用F#解析器库FParsec实现公式求导符号计算。

1 文本解析到Expr

首先,在F#项目中引入FParsec库,并定义一个Expr类型:

type Expr = 
    | CstF of float
    | Var of string
    | Add of Expr * Expr  // +
    | Sub of Expr * Expr // -
    | Mul of Expr * Expr // *
    | Div of Expr * Expr // / 
    | Pow of Expr * Expr // ^ 
    | Sin of Expr  
    | Cos of Expr 
    | Exp of Expr 
    | Ln of Expr 
    | Neg of Expr 
    | Factorial of Expr 
    | Error of string

        这个类型中包含了变量Var,数值CstF,加减乘除运算符,三角函数等。其次,再利用FParsec库提供的强大解析功能,给出一些解析工具方法。这些方法有的可以解析浮点类型,有的可以解析从小括号中提取表达式,具体示例如下:

//忽略空白字符
let private ws = CharParsers.spaces
//忽略特定字符并忽略末尾的空白字符
let private ch c = CharParsers.skipChar c >>. ws
//解析浮点类型的值,忽略末尾的空白字符,并转换成 CstF 类型
let private num = CharParsers.pfloat .>> ws  |>> (fun x -> CstF x)
//变量标识符规则
let private identifier =
        let isIdentifierFirstChar c = isLetter c || c = '_'
        let isIdentifierChar c = isLetter c || isDigit c || c = '_'  
        many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier"
//忽略空白
let private identifierws =  spaces >>.  identifier  .>> spaces 
//变量解析,注意|>>与id对其,否则报错
let private  id = identifierws
                   |>>(fun x -> Var x)
                   .>>ws

//创建一个新的具有优先级的操作符解析器
let private opp = new OperatorPrecedenceParser<_,_,_>()
//重命名表达式解析器ExpressionParser,方便调用
let private expr = opp.ExpressionParser
//括号中提取表达式
let private bra_expr = ch '(' >>. expr .>> ch ')'
// 定义支持的术语terms,即操作符外的类型解析器
//id表示变量解析器,num表示浮点类型解析器,bra_expr表示表达式解析器
let private terms = choice[ id; num; bra_expr]
opp.TermParser <- terms

opp.AddOperator(InfixOperator("+", ws,1, Associativity.Left, fun x y -> Add(x, y)))
opp.AddOperator(InfixOperator("-", ws,1, Associativity.Left, fun x y -> Sub(x, y)))
opp.AddOperator(InfixOperator("*", ws,2, Associativity.Left, fun x y -> Mul(x, y)))
opp.AddOperator(InfixOperator("/", ws,2, Associativity.Left, fun x y -> Div(x, y)))
opp.AddOperator(InfixOperator("^", ws,3, Associativity.Left, fun x y -> Pow(x, y)))

opp.AddOperator(PrefixOperator("sin", ws,4, true, fun x  -> Sin(x)))
opp.AddOperator(PrefixOperator("cos", ws,4, true, fun x  -> Cos(x)))
opp.AddOperator(PrefixOperator("exp", ws,4, true, fun x  -> Exp(x)))
opp.AddOperator(PrefixOperator("ln", ws,4, true, fun x  -> Ln(x)))

opp.AddOperator(PostfixOperator("!", ws,5, true, fun x  -> Factorial(x)))

//忽略空白
let private expr_ws = ws >>. expr .>> ws
//调用run方法调用字符解析器
let private parse s = CharParsers.run expr_ws s

        这里需要注意一下,不少方法定义的时候的有关键字private限定,这表示私有的,在模块内可以访问,但在模块外无法直接访问。下面再定义一个公有方法,可以在其他模块进行访问。具体示例如下:

let strParser s = 
    match parse s with
    | Success(value, _, _)   -> value
    | Failure(err, _, _) -> Error err 

     一般来说,一个函数返回的类型是一致的,由于parse解析文本后,返回的类型可以是成功的Success,也可以是失败的Failure,为了兼容考虑,这里定义了一个Error 类型,它也是一个Expr类型。而Success返回的value就是一个Expr类型。

2 Expr求导计算

      下面,给出Expr类型的求导函数,具体的示例如下:

let rec private diff e  =
    match e with
    | CstF f                -> CstF 0.0
    | Var x                 -> CstF 1.0 
    | Add(CstF a, Var x)   ->  CstF 1.0 
    | Add(e1, e2)  -> Add(diff e1, diff e2)
    | Sub(e1, e2)  -> Sub(diff e1, diff e2)
    | Mul(CstF a, Var x) ->  CstF a 
    | Mul(e1, e2) ->  Mul(diff e1, diff e2)
    | Pow(Var x,CstF a) -> Mul(CstF a,Pow(Var x,CstF (a - 1.)))
    | Pow(e1,e2)  -> Mul(e2,Pow(e1, Sub(e2,CstF 1.)))
    | Sin(e1)  -> Mul(Cos(e1),diff e1)
    | Cos(e1)  -> Mul(Neg(Sin(e1)),diff e1)
    | Neg(e1)  -> Neg(diff e1)
    | e          -> e 


let rec private printExpr e =
    match e with
    | CstF f            -> string f
    | Var x             ->  x 
    | Add(e1 , e2) ->  "(" + (printExpr e1) + "+" + (printExpr e2) + ")"
    | Sub(e1 , e2) ->  "(" + (printExpr e1) + "-" + (printExpr e2) + ")"
    | Mul(e1 , e2) ->  "(" + (printExpr e1) + "*" + (printExpr e2) + ")"
    | Div(e1 , e2) ->  "(" + (printExpr e1) + "/" + (printExpr e2) + ")"
    | Pow(e1 , e2) ->  "(" + (printExpr e1) + "^" + (printExpr e2) + ")"
    | Sin(e1) ->  "sin(" + (printExpr e1) + ")"
    | Cos(e1) ->  "cos(" + (printExpr e1) + ")"
    | Neg(e1) ->  "-(" + (printExpr e1) + ")"
    | _          -> failwith "printExpr error"

let diff1 s  = diff (strParser s) |> printExpr

    其中的diff是一个private内部方法,且用rec关键字限定说明是一个递归函数。这里需要注意一下,rec在private关键字之前,而不能调换位置。同理,printExpr函数则是将Expr类型的表达式输出为文本类型的数学公式。

     最后,定义一个diff1 函数,它接受1个文本类型的输入,首先通过strParser s 解析成Expr类型的对象,然后调用diff函数进行公式求导,并将求导后的结果作为参数传递给函数printExpr进行打印输出。

3 求导测试

      最后,给出求导公式的测试程序:

open System
open Yd.ExpParser 
[<EntryPoint>]
let main argv =
     Console.WriteLine "Welcome YdCAS Demo【JackWangCUMT】";
      Console.WriteLine "目前只支持求导:sin(2*x) - > (cos((2*x))*2)";
     Console.Write "$>";
     let mutable input = Console.ReadLine()
     while input<>"quit" do
            printfn "diff(%s) => %O" input (diff1 input)
            Console.Write "$>";
            //input = Console.ReadLine() //不能修改初始值
            input <-  Console.ReadLine() //可以
     printfn "%s" input
     0

     运行并输入如下测试用例,结果为:

Welcome YdCAS Demo【JackWangCUMT】
目前只支持求导:sin(2*x) - > (cos((2*x))*2)
$>x^3
diff(x^3) => (3*(x^2))
$>sin(x^2+3*x)
diff(sin(x^2+3*x)) => (cos(((x^2)+(3*x)))*((2*(x^1))+3))
$>x^2 + 3 *x
diff(x^2 + 3 *x) => ((2*(x^1))+3)
$>cos(x)
diff(cos(x)) => (-(sin(x))*1)
$>

这里有一个需要注意的就是:mutable input 用关键字mutable 修饰表示可变的变量,且在while循环体中,修改值用 <- 而不能是 = ,否则值不能修改。

11.png

(完)