NAG F# 示例
David Carlisle, March 2010
目录
F# — 简介
F# 是一个源自于 OCAML 的函数程序语言, 它与 标准 ML 语言,都有一个相同的 源头, 在二十多年前,我在曼彻斯特大学定理证明系统上就已经使用过…
F# 使用许多来自 OCAML 的衍生语法,但在其接口中也增加了许多功能,能够更方便的与 .NET 架构互通 (因为它编译成与其他 .NET 语言相同的 CLR 虚拟机)。 因此,我们能够很容易地在 F# 中调用 NAG .NET 算法库,然而这些界面并不是惯用的函数程序语言调用协议。 在本文中,我希望能够展现这些原因,而且显示如何透过简单几行的 F#,就能够开发调用 NAG 算法库的界面,可以提供更自然的使用函数,并因应应用系统的特殊需求进行调试。
F# 虽然具有强大功能的基础,但却是非常具有陈述性的语言,能够支持许多不同的编程风格。也因此,其具备可以定义函数 (方法) 成不同的风格形式。 ML 语言家族中最显着的特点是 强型别,但 (限制于核心函数功能) 并不需要被明确的定义,表示式是在编译期进行分析并进而推论出。
F# 有两种方式可以使用;做为一个编译器 (编译成 CIL 码,如同 C# 或 VB.NET),也可以当作提供 read eval print 的交互式直译器, 对 lisp 编程人员与 MATLAB 或 Maple 系统的用户来说,是相当孰悉的。
在了解 NAG 算法库的示例之前,我们先来看看一个简单的示例:将两个数值相加。
相加示例
我们第一个例子中使用最为孰悉的一个样式。请注意与其他许多命令式语言不同,F# 函数仅仅只有单一参数。此处 (a,b) 并不是表示
成对的两个数值,而是单一的值,整数对 (以 int * int 型态表示)。
// sum_tuple declaration > let sum_tuple (a,b) = a + b;; val sum_tuple : int * int -> int
较为惯用的函数形式是扩充形式,取代连续输入数值的形式,而改为函数取得整数,依序传回函数并应用到第二个整数,传回相加结果。
// sum_curried declaration > let sum_curried a b = a + b;; val sum_curried : int -> int -> int
以及利用变量储存数值 (在此例中 int),ML 基础的语言允许变量储存数值 (这里是 int ref) 的参考 (指标)。
赋值的操作数 := 更新参考到的数值 (链接到它本身的参考并不会改变)。 传递这样的引用类型是在命令式语言中仿真函数或子程序
“输出参数” 的方法。回传的型别 unit,是类似于 C 函数中的 void 或 Fortran 语言中的子程序。
// sum_ref declaration > let sum_ref (a,b,r) = r := a + b;; val sum_ref : int * int * int ref -> unit
除了衍申自 ML 的引用类型外,F# 提供依引用类型传递数值的 .NET 机制的语法。以下的示例采用了标注型别 r:byref<int> 方式
来强迫使用这样的机制,并且使用赋值的操作数 <- 而不是 := 去更新数值。
依引用类型传递数值而不是传递引用类型,在原始 F# 程序中并不会经常发生,然而以其他语言所撰写的 .NET 算法库方法,通常都是源自于这样的形式。
// sum_byref declaration > let sum_byref (a,b,r:byref<int>) = r <- a + b;; val sum_byref : int * int * byref<int> -> unit
这四种定义的方式,可以用来计算 3 = 1 + 2:
// Simple sum examples
> let r1 = sum_tuple (1,2);;
val r1 : int = 3
> let r2 = sum_curried 1 2;;
val r2 : int = 3
> let rr3 = ref 0;;
val rr3 : int ref = {contents = 0;}
> sum_ref (1,2,rr3);;
val it : unit = ()
> let r3 = !rr3;;
val r3 : int = 3
> let mutable r4 = 0;;
val mutable r4 : int = 0
> sum_byref(1,2,&r4);;
val it : unit = ()
> r4;;
val it : int = 3
前面两种方式显然是最为简便的,其后的两个形式,都需要声明变量来储存结果,也并不难理解,且对使用过其他命令式语言的人来说也是相当孰悉的形式。
F# 设计的本质就是函数,它强调的重点是函数的方法,而且能够充分使用任何 .NET 方法,
即便这些方法是依命令式或面向对象所设计的,并非是函数式、声明的接口。
在这些例子中,参考的使用是比较夸张的,我使用交互式 F# 系统,并利用 ;; 终止符让系统响应计算值与表示式。
这是一个相当有效的方法,可以让我们了解会发生甚么情况,但在实务上,我们通常会使用编译器,不会让程序都响应,
或者在交互式系统中的每一行结尾不使用 ;;。
函数已经组合好时,在函数程序语言如 F# 中使用元组或扩充形式,会有明显的优势。 如果想要计算 (1+1) + (2+2) (先忽略 (1+1) + (2+2) 在 F# 中是有效的) 那么 sum_tuple 与 sum_curried 的例子便不会有副作用,函数传回的结果可以 用非常自然的方式组成:
// composing sum examples (functional) > let c1=sum_tuple (sum_tuple(1,1),sum_tuple(2,2));; val c1 : int = 6 > let c2=sum_curried (sum_curried 1 1) (sum_curried 2 2);; val c2 : int = 6
另一方面,sum_ref 与 sum_byref 的设计将会有更为严谨的调用方式,必须要声明临时的变量来储存 (参考) 计算中间的结果。
// composing sum examples (imperative)
> let r3a = ref 0;;
val r3a : int ref = {contents = 0;}
> let r3b = ref 0;;
val r3b : int ref = {contents = 0;}
> let r3c = ref 0;;
val r3c : int ref = {contents = 0;}
> sum_ref (1,1,r3a);;
val it : unit = ()
> sum_ref (2,2,r3b);;
val it : unit = ()
> sum_ref (!r3a,!r3b,r3c);;
val it : unit = ()
> let c3 = !r3c;;
val c3 : int = 6
> let mutable r4a = 0;;
val mutable r4a : int = 0
> let mutable r4b = 0;;
val mutable r4b : int = 0
> let mutable c4 = 0;;
val mutable c4 : int = 0
> sum_byref (1,1,&r4a);;
val it : unit = ()
> sum_byref (2,2,&r4b);;
val it : unit = ()
> sum_byref (r4a,r4b,&c4);;
val it : unit = ()
> c4;;
val it : int = 6
如果你已经拥有提供 sum_ref 的算法库,但是想要以更自然的方式 (在 F# 中) 使用,那么我们可以很容易的 "包裹"
sum_ref 函数,如以下的例子:
// mysum declaration and use > let mysum a b = let r = ref 0 sum_ref(a,b,r) !r ;; val mysum : int -> int -> int > let c5=mysum(mysum 1 1) (mysum 2 2);; val c5 : int = 6
看完这个 "相加" 的示例,我们将继续介绍实际调用 NAG .NET 算法库的例子。
NAG .NET 算法库
在参考 NAG 库之前有些细节需要留意。#r 语法仅在交互式的环境中使用。对于 F# 编译器来说,命令行参数将会在 Visual Studio 环境中由其他设定所取代。我们使用 “open” 来开启 System 与 NagLibrary 命名空间是为了避免
每次调用方法时都要提供完整的方法名称。
// NAG Library reference for fsi > #if INTERACTIVE #I @"c:\Program Files\NAG\NAG Library for .NET" #r "NagLibrary32.dll" #endif open System open NagLibrary --> Added 'c:\Program Files\NAG\NAG Library for .NET' to library include path --> Referenced 'c:\Program Files\NAG\NAG Library for .NET\NagLibrary32.dll'
A00:产品授权验证
我们调用算法库中最简单的一个例子,当合法的 NAG 授权被确认则会回传 true。
// a00ac example > printfn "Have a NAG licence: %b" (A00.a00ac()) Have a NAG licence: true val it : unit = ()
这是一个相当简单的例子,它显示出许多函数程序语言与高阶互动循环的优点。类似于 MATLAB,F# 的接口能够让用户利用最少的语法与编程步骤就能使用 NAG 算法库。 不需要主程序、不需要额外的变量定义,也不需要明确的编译步骤等。
D01: 积分
接下来我们来看一个稍微复杂的例子,利用积分方法求解。积分函数 (方法) 结合了用户输入的被积函数与设定的边界值,并会传回额外关于迭带次数与正确性的相关资讯。 虽然我们可以很直接的调用此方法,透过 byref 参数取得结果,却需要以类似上面所提的 sum_byref 方式,明确的转换被积函数,而不是一般更为惯用的方法 sum_curried。
这里是一个例子,有两种以参考方式设定参数的风格。典型的定义参考型态的 ML 方式,须以 ! 解除参考,以及另一种利用 npts 的替代语法,使用可变的变量类似于命令式语言的变量方式,会自动的解除参考。
// d01ah example: direct call
> let if1 = ref 0
let mutable npts=0
let relerr= ref 0.0
let f x = Math.Sin(x:double)
let nlimit=1000
let ff= D01.D01AH_F(f)
let result1 = D01.d01ah(
0.0,Math.PI,0.01,&npts,relerr,ff,nlimit, if1);;
val mutable npts : int = 7
val relerr : float ref = {contents = 0.0006944567037;}
val f : float -> float
val nlimit : int = 1000
val ff : D01.D01AH_F
val result1 : float = 2.0
> !if1;;
val it : int = 0
> npts;;
val it : int = 7
> !relerr;
val it : float = 0.0006944567037
F# 有许多功能能够以更函数型态的方式自动呈现输出的参数。如果在 .NET 方法的最后,以参考方式传递参数, F# 提供可选择的签章 (Signatures),能在方法被调用时省略参数,回传的数值会以串行的方式提供。 如果方法是以兼容的匿名函数 (lambda 表示式) 传递,F# 中则隐含了应用委托建构函式 (delegate constructor),即便它不会自动的系结到适当的委托类型。
在此情况下,我们可以忽略最后的 ifail 参数,它的值 (0) 是在回传的序列值中的第二个,而不是被参考参数 (上例中的 ifl ) 所抓取到值。
输入参数 nlimit 的所在位置是为了防止这样自动的方式,以避免对 npts 明确的指定参考。
同时,也不传递 ff 变量,因为我们使用 D01.D01AH_FC 做为一个匿名函数。
// d01ah example: returning a tuple
> let (result2,if2) = D01.d01ah(
0.0,Math.PI,0.01,&npts, relerr,(fun x -> f x),nlimit);;
val result2 : float = 2.0
val if2 : int = 0
> npts;;
val it : int = 7
> !relerr;;
val it : float = 0.0006944567037
如同以上的 mysum 例子,如果方法是经常会被调用到的,更为自然的函数接口也许是更好的方法。
选择正确的接口大部分取决于个人所需,但也与个人的美学判断有关。在以下的例子中,我对输入参数采用扩充形式, 对于输出字段以记录类型撷取,但对于其他来说,则可以对输入采用元组形式,并对结果以元组形式撷取 (或者采用输出参数)。
// declaration of myd01ah: curried, returning a record
> type d01ahresults = {result: float ; npts: int ; relerr: float; ifail: int}
> let myd01ah f interval epsr nlimit =
let ff=D01.D01AH_F(f)
let (a,b) = interval
let npts = ref 0
let relerr = ref 0.0
let ifl = ref 0
let result = D01.d01ah(a,b,epsr,npts,relerr,ff,nlimit,ifl)
{result= result; npts= !npts;relerr= !relerr;ifail= !ifl} ;;
val myd01ah :
(float -> float) -> float * float -> float -> int -> d01ahresults
透过这样的设计的方法,我们可以直接指定特定的间隔并且加入 Math.Sin,不需要定义任何附带的参考或委托的对象。所以全部的 d01ah 程序可以被压缩成一行。
// using myd01ah
> myd01ah Math.Sin (0.0,Math.PI) 0.01 1000 ;;
val it : d01ahresults = {result = 2.0;
npts = 7;
relerr = 0.0006944567037;
ifail = 0;}
E04:优化示例
最后,我们提供更为复杂的优化求解示例。必须提供参考参数的的回调函式 (callback function),而且有许多上层方法会参考到的参数。
这里使用到的做法类似于 D01 示例,对输入参数使用扩充形式,以纪录型别读取结果。 除此之外,原来接口中会用到用来表明区间的一对实数参数,已经被结合到单一的输入参数,以及单一的结果字段。
// declaration of mye04ab: curried, returning a record
> type e04abresults = { minimum: float ; x: float; bounds: (float*float);
iter: int; ifail: int }
> let mye04ab f interv e1 e2 mc =
let a,b = interv
let ra = ref a
let rb = ref b
let fx = ref 0.0
let x = ref 0.0
let mcr = ref mc
let ifr = ref 0
E04.e04ab((fun x res -> res <- f x),
ref e1, ref e2, ra, rb, mcr ,x, fx ,ifr)
{minimum = !fx; x = !x;bounds = (!ra,!rb); iter= !mcr; ifail = !ifr} ;;
val mye04ab :
(float -> float) -> float * float -> float -> float -> int -> e04abresults
当然,当这个方法被定义好后,我们可以改为更直接调用的方式,不需要去参考变量或委托。
// using mye04ab
> mye04ab (fun x -> (Math.Sin x) / x) (3.0,5.0) 0.0 0.0 30 ;;
val it : e04abresults = {minimum = -0.2172336282;
x = 4.493409455;
bounds = (4.493409397, 4.493409513);
iter = 10;
ifail = 0;}
输出结果
NAG .NET 算法库中的方法都是透过参考参数取得其计算结果 (或者是函数传回值),然而,在以下的情况中,文字输出可以直接导向至输出串流:
- 在 .NET 接口所侦测的输入错误与发生
NagException型别例外时。对控制面板应用程序而言, 将会将这些讯息显示在控制台中。其他的应用程序能够 catch 这样的例外并能使用标准的 .NET 技术处理。 - 由算法库所产生的错误与警告讯息,并不会丢出 .NET 例外。方法会传回非 0 的
ifail值,在这样的情况下,讯息会送至标准输出串流。 通常,对控制面板应用程序而言,将会将这些讯息显示在控制台中;其他的应用程序可以使用System.Console.Redirect方法,将这些讯息导向到不同的串流中。 - 在算法库中有某些方法可以显示求解过程中的监测资讯。在某些情况下来说,监控的控制是由用户所提供的函数来管理的。
算法库中提供的 C# 示例程序利用
Console.WriteLine来实现监控的回调函示。由使用者提供的方法可以直接将 监测的讯息转移到其他合适的地方,例如:Windows 应用系统中的文字框。其他的情况下,监控的控制是由整数或布尔值来驱动,那么现有的版本,可以采用Console.WriteLine方式,无论如何,最终的输出串流可以由以上所提的方式来控制。
下面两个示例,我们以 windows 应用程序呈现 (取自 Microsoft 所提供的 F# 示例程序)。
监测函数 (e04cb 示例)
e04cb 是求优化问题函数,由用户提供函数,并监控求解中每个迭代的过程。我们如同先前定义一个较小的 F# 程序 (这次我们传回变量的元组,而不是记录型别)。
// declaration of mye04cb
let mye04cb n x tolf tolx func monit maxcal =
let ffunc = new E04.E04CB_FUNCT(
fun n xc (res:byref<float>) -> res <- func xc)
let fmonit = new E04.E04CB_MONIT(monit)
let ifl = ref 0
let rf = ref 0.0
E04.e04cb(n,x,rf,tolx,tolf,ffunc,fmonit,maxcal,ifl)
(x,!rf)
我们让程序简化,且以数值取代边界变量。
// object function for e04cb example
let mutable e04cbf = fun (xc: float[]) ->
Math.Exp(xc.[0]) * (4.00e0 * xc.[0] * (xc.[0] + xc.[1]) +
2.00e0 * xc.[1] * (xc.[1] + 1.00e0) +
1.00e0);
// e04cb example bounds let mutable e04cbx = [|-1.0;1.0|]
本例中我们将使用简化的监测函数,仅仅显示出迭代的次数。我们使用 windows 形式的应用程序,并不将此结果显示在控制面板,
我们采用文字框 (textB) 并将监测的文字结果更新在文字框中:
// e04cb monit function
let e04cbmonitor fmin fmax sim n ncall se vr =
textB.Text <- ( textB.Text +
(sprintf " There have been %d function calls\n" ncall))
当執行 "Run Example 1" 选项后,则 runexmpl1 方法将会被執行:
// run the e04cb example
let runexmpl1 () =
textB.Text <- "monitor\n"
textB2.Text <- sprintf "The final function value is ...\n"
let (interv,vl) = mye04cb 2 e04cbx (Math.Sqrt(X02.x02aj()))
(Math.Sqrt(Math.Sqrt(X02.x02aj())))
e04cbf
e04cbmonitor
100
textB2.Text <- ( textB2.Text + (sprintf " %f \n" vl))
接着我们可以看到,启动了两个文字框,并调用 mye04cb,然后最终取得最小值并更新文字框。
在迭代的过程中,将会透过 e04cbmonitor 对文字框进行更新。
如果此应用程序是由 F# 编译器執行的 (fsi 命令或在 Visual Studio 的 F# 交互式环境),那么我们可以使用上面定义去改变 目标函数,而不需要重新启动应用程序。输入 (例如):
// Update e04cb object function e04cbf <- fun (xc: float[]) -> 1.0;;
在交互式的环境中,将会让 e04cbf 的变量进行更新 (表示常数函数 1),重新选择“Run e04cb example” 选项,
将能让 e04cb 以新的目标函数进行求解。F# 未来的版本或许会将直译器视为一个功能 (例如在 lisp 或其他函数式程序语言),
这样能避免需要维持一个开放式的直译器窗口,无论如何,利用这样的方式是个相当强大的方式去跟系统互动。
内部求解监测 (e04xa 示例)
在算法库中有许多的方法并不会采用用户提供的监测函数,而是由内部所控制,通常是由整数参数 (或可选参数) 所控制,来调试输出讯息的多寡。
目前我们提供的监测接口,是由 System.Console.WriteLine 方法来处理。在未来推出的 NAG .NET 算法库版本中,我们将会提供更为便利的接口,
无论如何,不论我们是否生成一个 console application,这样的监控接口依然是有用的。
我们的技术是将 Console 方法的输出结果导向一个内部串流,并将其转为字符串,例如更新字符串区块。我们以 e04xa 方法来说明这样的方式是可行的。
// declaration of mye04xa
let mye04xa mlevel n epsrf x mode objfun hforw =
let objf = ref 0.0
let objgrd :float array = Array.zeroCreate n
let hcntrl :float array = Array.zeroCreate n
let h :float [,] = Array2D.zeroCreate n n
let info : int array = Array.zeroCreate n
let mref = ref mode
let objfref = ref 0.0
let warnref = ref 0
let ifailref = ref 0
let objfund = E04.E04XA_OBJFUN(
fun (a:byref<int>) b c (d:byref<float>) e f ->
d <- objfun a b c d e f
)
E04.e04xa (mlevel,n,epsrf,x,mref,objfund,hforw,objfref,objgrd,
hcntrl,h,warnref,info,ifailref)
(!objfref,objgrd,hcntrl,h,!warnref)
// e04xa object function let e04xaobjfun int n (x: float array) objf objgrd nstate = let a = x.[0] + 10.0*x.[1] let b = x.[2] - x.[3] let c = x.[1] - 2.0*x.[2] let d = x.[0] - x.[3] a*a + 5.0*b*b + c*c*c*c + 10.0*d*d*d*d
当选择了“Run e04xa” 选项后,以下的方法便会被執行
// run the e04xa example
let runexmpl2 () =
textB.Text <- "monitor\n"
textB2.Text <- sprintf "The final function value is ...\n"
let oldOut = Console.Out
let strWriter = new IO.StringWriter()
Console.SetOut(strWriter);
let (objf,z1,z2,z3,z4)=
mye04xa
(*mlevel*) 10
(*n*) 4
(*epsrf*) 0.0
(*x*) [|3.0;-1.0;0.0;1.0|]
(*mode*) 0
(*objfun*) e04xaobjfun
(*hforw*) [|-1.0;-1.0;-1.0;-1.0|]
Console.SetOut(oldOut)
textB.Text <- strWriter.ToString()
textB2.Text <- ( textB2.Text + (sprintf " %f \n" objf))
这里所取得的监测讯息都会储存在 strWriter 中,再来 (当執行完 e04xa 后) 此字符串变量会用来更新文本块。
如上所述,我们必须在往后的版本中提供更为直接的接口。
F# 文件
nagfsharp1.fsx:F# 包含了所有以上的示例程序。
nagfsharp2.fsx:F# 档包含了完整的 windows form application 程序代码,有下拉选项執行两个 提供过程监控的示例。基本上这是由 Microsoft 所提供的 F# “Hello World”程序延伸的,并加入程序執行 前述的 NAG 示例。
nagfsharp3.fsx:额外示例,在 F# 中调用 e04cb 函数。
结论
本文中提供了许多示例,说明如何自 F# 调用 NAG .NET 算法库,尤其是如何针对算法库提供的方法写一个函数来简化调用的方式。
有人问我们是否会提供更类似于 F# 的 .NET 方法。这是有可能的,例如将所有 out 或者 ref 参数放在方法参数列的最后,
然而,这样并不会让 C# 或 VB.NET 有更为自然的接口,而且 F# 的用户或许也希望能够以简单的包裹程序,来避免需要明确声明的参数、
或者针对输出使用命名纪录、或者采用以上例子中所呈现的输入模式。
了解更多 NAG .NET 算法库