在之前的博客中,我已经介绍了一下如何用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循环体中,修改值用 <- 而不能是 = ,否则值不能修改。