在之前的博客中,我已经介绍了一下如何用F#对数学表达式进行求导,但是其中有一个问题,就是求导的对象是一个Expr类型,这样使用起来是很不方便的。一般来说,用户使用都是直接输入一个文本类型的数学公式,然后求导系统应该就可以对其进行自动解析成Expr类型,并根据求导规则进行求导的符号计算。
如果对之前撰写的博文【F#表达式求导】,感兴趣的话可以自行去阅读。下面,给出利用F#解析器库FParsec实现公式求导符号计算。
1 文本解析到Expr
首先,在F#项目中引入FParsec库,并定义一个Expr类型:
typeExpr=|CstFoffloat|Varofstring|AddofExpr*Expr// +|SubofExpr*Expr// -|MulofExpr*Expr// *|DivofExpr*Expr// / |PowofExpr*Expr// ^ |SinofExpr|CosofExpr|ExpofExpr|LnofExpr|NegofExpr|FactorialofExpr|Errorofstring
这个类型中包含了变量Var,数值CstF,加减乘除运算符,三角函数等。其次,再利用FParsec库提供的强大解析功能,给出一些解析工具方法。这些方法有的可以解析浮点类型,有的可以解析从小括号中提取表达式,具体示例如下:
//忽略空白字符letprivatews=CharParsers.spaces//忽略特定字符并忽略末尾的空白字符letprivatechc=CharParsers.skipCharc>>. ws//解析浮点类型的值,忽略末尾的空白字符,并转换成 CstF 类型letprivatenum=CharParsers.pfloat .>>ws|>> (funx->CstFx) //变量标识符规则letprivateidentifier=letisIdentifierFirstCharc=isLetterc||c='_'letisIdentifierCharc=isLetterc||isDigitc||c='_'many1Satisfy2LisIdentifierFirstCharisIdentifierChar"identifier"//忽略空白letprivateidentifierws=spaces>>. identifier .>>spaces//变量解析,注意|>>与id对其,否则报错letprivateid=identifierws|>>(funx->Varx) .>>ws//创建一个新的具有优先级的操作符解析器letprivateopp=newOperatorPrecedenceParser<_,_,_>() //重命名表达式解析器ExpressionParser,方便调用letprivateexpr=opp.ExpressionParser//括号中提取表达式letprivatebra_expr=ch'('>>. expr .>>ch')'// 定义支持的术语terms,即操作符外的类型解析器//id表示变量解析器,num表示浮点类型解析器,bra_expr表示表达式解析器letprivateterms=choice[ id; num; bra_expr] opp.TermParser<-termsopp.AddOperator(InfixOperator("+", ws,1, Associativity.Left, funxy->Add(x, y))) opp.AddOperator(InfixOperator("-", ws,1, Associativity.Left, funxy->Sub(x, y))) opp.AddOperator(InfixOperator("*", ws,2, Associativity.Left, funxy->Mul(x, y))) opp.AddOperator(InfixOperator("/", ws,2, Associativity.Left, funxy->Div(x, y))) opp.AddOperator(InfixOperator("^", ws,3, Associativity.Left, funxy->Pow(x, y))) opp.AddOperator(PrefixOperator("sin", ws,4, true, funx->Sin(x))) opp.AddOperator(PrefixOperator("cos", ws,4, true, funx->Cos(x))) opp.AddOperator(PrefixOperator("exp", ws,4, true, funx->Exp(x))) opp.AddOperator(PrefixOperator("ln", ws,4, true, funx->Ln(x))) opp.AddOperator(PostfixOperator("!", ws,5, true, funx->Factorial(x))) //忽略空白letprivateexpr_ws=ws>>. expr .>>ws//调用run方法调用字符解析器letprivateparses=CharParsers.runexpr_wss
这里需要注意一下,不少方法定义的时候的有关键字private限定,这表示私有的,在模块内可以访问,但在模块外无法直接访问。下面再定义一个公有方法,可以在其他模块进行访问。具体示例如下:
letstrParsers=matchparseswith|Success(value, _, _) ->value|Failure(err, _, _) ->Errorerr
一般来说,一个函数返回的类型是一致的,由于parse解析文本后,返回的类型可以是成功的Success,也可以是失败的Failure,为了兼容考虑,这里定义了一个Error 类型,它也是一个Expr类型。而Success返回的value就是一个Expr类型。
2 Expr求导计算
下面,给出Expr类型的求导函数,具体的示例如下:
letrecprivatediffe=matchewith|CstFf->CstF0.0|Varx->CstF1.0|Add(CstFa, Varx) ->CstF1.0|Add(e1, e2) ->Add(diffe1, diffe2) |Sub(e1, e2) ->Sub(diffe1, diffe2) |Mul(CstFa, Varx) ->CstFa|Mul(e1, e2) ->Mul(diffe1, diffe2) |Pow(Varx,CstFa) ->Mul(CstFa,Pow(Varx,CstF (a-1.))) |Pow(e1,e2) ->Mul(e2,Pow(e1, Sub(e2,CstF1.))) |Sin(e1) ->Mul(Cos(e1),diffe1) |Cos(e1) ->Mul(Neg(Sin(e1)),diffe1) |Neg(e1) ->Neg(diffe1) |e->eletrecprivateprintExpre=matchewith|CstFf->stringf|Varx->x|Add(e1 , e2) ->"("+ (printExpre1) +"+"+ (printExpre2) +")"|Sub(e1 , e2) ->"("+ (printExpre1) +"-"+ (printExpre2) +")"|Mul(e1 , e2) ->"("+ (printExpre1) +"*"+ (printExpre2) +")"|Div(e1 , e2) ->"("+ (printExpre1) +"/"+ (printExpre2) +")"|Pow(e1 , e2) ->"("+ (printExpre1) +"^"+ (printExpre2) +")"|Sin(e1) ->"sin("+ (printExpre1) +")"|Cos(e1) ->"cos("+ (printExpre1) +")"|Neg(e1) ->"-("+ (printExpre1) +")"|_->failwith"printExpr error"letdiff1s=diff (strParsers) |>printExpr
其中的diff是一个private内部方法,且用rec关键字限定说明是一个递归函数。这里需要注意一下,rec在private关键字之前,而不能调换位置。同理,printExpr函数则是将Expr类型的表达式输出为文本类型的数学公式。
最后,定义一个diff1 函数,它接受1个文本类型的输入,首先通过strParser s 解析成Expr类型的对象,然后调用diff函数进行公式求导,并将求导后的结果作为参数传递给函数printExpr进行打印输出。
3 求导测试
最后,给出求导公式的测试程序:
openSystemopenYd.ExpParser[<EntryPoint>] letmainargv=Console.WriteLine"Welcome YdCAS Demo【JackWangCUMT】"; Console.WriteLine"目前只支持求导:sin(2*x) - > (cos((2*x))*2)"; Console.Write"$>"; letmutableinput=Console.ReadLine() whileinput<>"quit"doprintfn"diff(%s) => %O"input (diff1input) Console.Write"$>"; //input = Console.ReadLine() //不能修改初始值input<-Console.ReadLine() //可以printfn"%s"input0
运行并输入如下测试用例,结果为:
WelcomeYdCASDemo【JackWangCUMT】目前只支持求导:sin(2*x) -> (cos((2*x))*2) $>x^3diff(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*xdiff(x^2+3*x) => ((2*(x^1))+3) $>cos(x) diff(cos(x)) => (-(sin(x))*1) $>