NAG F# 示例

David Carlisle, March 2010

目录

    D01: 积分
    E04: 优化
    输出结果

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 对文字框进行更新。

fsharp form executed from Visual Studio

如果此应用程序是由 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 算法库