使用嵌套 Lambda 函数实现数值算法






4.88/5 (8投票s)
对于数值计算,使用 Lambda 函数可以弥补 C# 中缺少嵌套方法的不足。
引言
大约二十七年前,我曾用 Pascal 编写过一些数值方法。许多算法都使用了嵌套函数。本文介绍了如何使用嵌套 Lambda 函数,将其中许多函数成功迁移到 C# 4.0。
我当时正在设计一艘游艇,问题是如何不仅要得到一个光滑的曲面,还要计算排水量、浮心和湿表面积等由船体表面定义产生的其他数据。当时还没有可以在我的 BBC 电脑上运行的 CAD 程序。它配备了一个 8 位 6502 处理器,运行速度为 2 MHz,内存为 32 KB。实际上,当时船体线图都是按全尺寸绘制的,并通过柔性木条或样条进行平滑处理。测量是从放样线或比例图上获得的,并使用辛普森法则通过手工计算来确定排水量、浮心以及由船体形状产生的其他数据。
当时,BBC 电脑有一个 Pascal 编译器。我还能够编写一个指定船体的函数。然后,通过使用数值计算,我就可以生成所需的船体数据。此外,我还能够迭代地修改指定船体的参数,除了其他理想的功能外,还能在满足所需倾侧力矩的情况下优化湿表面积。不可否认,当时的图形功能非常基础,但通过使用一般的圆锥曲线作为光滑曲线,可以获得一个光滑的船体。
这艘 12 米的游艇“阿波罗尼乌斯”(注意那些圆锥曲线),是一艘巡航赛艇,已经成功完成了许多远洋比赛。如今,我更喜欢在巴斯海峡(塔斯马尼亚岛和澳大利亚大陆之间一段湍急的水域)的岛屿间巡游。
背景
言归正传。上周,我在整理一些旧纸板文件夹时,偶然发现了我的旧 Pascal 代码,这些代码是用一种早已过时的点阵打印机打印出来的,几乎还能辨认。内容大致如下:
program boatHull(input, output); type posint = 1..maxint; {Much code omitted - for brevity} function integral(function f(x : real) : real; a, b : real; n : posint) : real; type {types} ; var {vars}; begin {chosen algorithm} end {integral}; function doubleIntegral (function f(x, y : real) : real; function g(y : real): real; function h(y : real): real; a, b : real; m, n: posint ) : real; {end of parameters} function area(y: real): real; { first nested function; note g, h, and m are in scope} function argument(x : real) : real; { second nested function; note y is in scope} begin {argument} argument := f(x,y) end {argument}; begin {area} area := integral(argument, g(y), h(y), m) end {area}; begin {doubleIntegral} doubleIntegral:= integral(area, a, b, n) end {doubleIntegral}; begin {demo} {input, output code goes here} end. {boatHull}
我们可以看到,函数 integral
和 doubleIntegral
的作用域使它们对程序 boatHull 可用。但是,函数 doubleIntegral
能够调用函数 integral
。函数 area 嵌套在 doubleIntegral
中,并且只能供 doubleIntegral
使用。同样,函数参数也只能供 area 函数使用。
封装是使用 Pascal 函数 doubleIntegral
中的嵌套函数的决定性原因。嵌套函数 area 在 doubleIntegral
之外没有任何意义,函数参数也没有理由存在于 area 函数之外。
迁移到 C# 4.0
在我退休后,我为教育环境设计和实现在线 SQL 数据库应用程序。我在 .aspx 页面的代码隐藏中使用 C#。自然,我想知道如何使用与上述类似的编码原则,通过嵌套函数来实现双重积分。
在 C# 中,Pascal 函数对应于方法。但是,无法直接翻译代码,因为 C# 虽然支持嵌套类,但不支持嵌套方法。然而,对于 C# 来说相对较新的 Lambda 函数可以被嵌套。于是,我下载了 Visual C# 2010 Express(.NET 4.0 的一个消费者),准备尝试一下。
此时,如果读者不熟悉 Lambda 符号和委托,那么他们应该查阅相关的参考资料,因为这类资料非常多。
首先,在初步介绍之后,是一些委托
using System;
using System.Linq;
namespace Computationals
{
// Delegates
delegate double Function(double x);
delegate double Functionxy(double x, double y);
delegate double Integral(Function f, double a, double b, int n);
// for integration of function f over n subintervals from a to b
public class LambdaNumericals
{
接下来,是一些积分方法:首先是辛普森法
static Integral SimpInt()
{
// Simpson's rule
// integration of function f, over n (even) subintervals in the interval [a,b]
Integral integral = (f, a, b, n) =>
{
// n, not even, throw exception code should go here
double sum = 0;
double h = (b - a) / n;
for (int i = 0; i < n; i = i + 2)
sum = sum + (f(a + i * h) + 4 * f(a + (i + 1) * h) + f(a + (i + 2) * h)) * h / 3;
return sum;
};
return integral;
}
其次,是高斯-勒让德 4 点算法
static Integral GaussInt()
{
// 4-point Gaussian rule integration of the function f,
// over n subintervals in the interval (a,b)
Integral integral = (f, a, b, n) =>
{
double[] weight = new double[4];
weight[2] = 0.652145154862546;
weight[1] = weight[2];
weight[3] = 0.347854845137454;
weight[0] = weight[3];
double[] point = new double[4];
point[3] = 0.861136311594053;
point[0] = -point[3];
point[2] = 0.339981043584856;
point[1] = -point[2];
double sum = 0;
double h = (b - a) / n;
double xr, xl, m, c, s;
for (int i = 0; i < n; i++)
{
s = 0;
xl = a + i * h;
xr = xl + h;
m = (xr - xl) / 2;
c = (xr + xl) / 2;
for (int j = 0; j < 4; j++)
s = s + weight[j] * f(m * point[j] + c) * m;
sum = sum + s;
}
return sum;
};
return integral;
}
现在,我们可以将以上内容用于
// Double integration using nested lambda functions
// Inner integral dx, from x=g(y) to x=h(y), over m subintervals, using algorithm inInt
// Outer integral dy, from y=a to y=b, over n subintervals, using algorithm outInt
static double dubInt(Functionxy fxy, Function g, Function h, double a, double b,
int m, int n, Integral inInt, Integral outInt)
{
return outInt((double y) => inInt((double x) => fxy(x, y), g(y), h(y), m), a, b, n);
}
inInt 和 outInt 是接受积分函数(SimpInt() 或 GaussInt())的参数。
Pascal 的 doubleIntegral 函数变为 outInt(area, a ,b, n)。
Pascal 代码中的 area 函数对应于片段 (double y) => inInt(argument, g(y), h(y), m)。
最后,argument 函数对应于 (double x) => fxy(x, y)。
public static double p(double x, double y)
{
return x * y;
}
public static double f1(double x)
{
return x;
}
public static double f2(double x)
{
return 2 * x;
}
public static double sine(double x)
{
return Math.Sin(x);
}
然后,我们可以使用以下方式调用我们的积分方法
public static void Main()
{
Integral integrate1, integrate2;
integrate1 = SimpInt();
Console.WriteLine(integrate1(sine, 0, Math.PI, 60));
// result 2.00000008353986
integrate2 = GaussInt();
Console.WriteLine(integrate2(sine, 0, Math.PI, 15));
// result 2
Console.WriteLine(integrate2(Math.Sin, 0, Math.PI, 15));
// result 2
// analytically, exactly 2 for the above integrals –
// note the superior convergence of Gauss 4-point algorithm
// Now to test our double integration functtion
Console.WriteLine(dubInt(p, f1, f2, 1, 2, 4, 4, integrate1, integrate2));
// result 5.625
// analytically 5 5/8
}
}
看点
接着,通过类似的策略,我已经将 Lambda 方法论应用于了微分、二次微分、曲线长度、偏导数、表面积和曲率。包含示例的代码在下载文件中。
所演示的技术可以轻松地扩展到极坐标积分、三重积分以及圆柱坐标和球坐标积分。参数函数、样条和曲面以及微分方程都是 Lambda 函数的潜在应用。毫无疑问,这仅仅是冰山一角。
代码应用限制
总之,我必须强调,本文关注的是 Lambda 函数和嵌套 Lambda 函数应用于数值计算的技术。在库的上下文中,实际的数值算法当然会包含错误规范。此外,某些常量的最佳值依赖于机器。我对上述方法适用于特定应用的适用性不做任何建议。
尽管如此,一旦程序员熟悉了 Lambda 函数的语法,它们就有可能为非平凡的数值计算提供非常简洁和高效的入口。