函数式编程实践(1):高阶函数的使用
时间:2011-02-14 来源:乱世虾
回想Lambda在LINQ中的使用,我们就可轻易理解函数式编程的特征之一:函数作为参数传递(即“函数的函数”,函数作为自变量)。函数作为参数导致函数的复合(还记得高中数学中的f(g(x))吗)。经常用LINQ的人一定对此已经习以为常。
今天我们讨论函数式编程的另外一个特征,即高阶函数。所谓高阶函数,就是“返回函数的函数”(注意与上文“函数的函数”相区别,函数作为因变量)。举一个最简单的例子:
Func<int, Func<int, int>> f = x => y => x + y;
注意这个委托变量的声明方式,从中我们可以直接解读到,这个委托类型对应的函数是这样的:输入一个int,返回一个函数;返回的函数是这样的:输入一个int,返回一个int。
再看“f =”右边的Lambda表达式,它其实是这样:
x => (y => (x + y));
或者
x => { return y => x + y; };
这个函数就是一个返回函数的函数,即高阶函数。当我们输入外层函数的参数,例如指定1,则f(1)仍然是一个函数,它等于y => 1 + y这个函数,如果你乐意,可以给它取名叫g函数,它用于给一个数加1。 我们可以继续传入参数,例如f(1)(2),也就是g(2),也就是给2加1。
不知道这样解释是否清楚明白。也许你会说这有什么用,你永远不会用到。但是我就是这样一个人,会对不一样的体验着迷,我把这种方式用到了项目中。我的项目是城市规划应用,是一个Windows Forms实现的城市指标分析结果查看器。在项目中有如下的Paint事件处理器:
private void pictureBox1_Paint(object sender, PaintEventArgs e) { _display.g = e.Graphics; _display.Width = pictureBox1.Width; _display.Height = pictureBox1.Height; e.Graphics.Clear(Color.Black); if (cbShowResult.Checked) { _display.PaintValue(); } if (cbShowRoads.Checked) { _display.PaintRoad(); } if (cbShowParcels.Checked) { _display.PaintParcel(); } if (cbShowSpot.Checked) { _display.PaintSpot(); _display.PaintHover(); _display.PaintSelect(); } if (!string.IsNullOrEmpty(_display.ToolRuning)) { _display.PaintTool(); } }
可以看到,在事件处理器中分别完成了绘制结果颜色、绘制道路、绘制地块、绘制点状实体、绘制点状实体悬停提示、绘制点状实体选择提示,以及绘制工具功能等模块。其中除了最后一个PaintTool,其他都是原生的静态C#函数,但PaintTool不是,它是一个Action类型的委托变量(Action委托对应不带参数、不返回值的函数)。
public Action PaintTool = () => { };
PaintTool的作用是为所有的工具进行必要的绘制,我的程序中有很多这样的工具功能,例如地块选择工具,需要根据鼠标位置高亮显示地块;测量工具,根据鼠标点选位置绘制折线;等等。如果使用传统方法,势必需要带参数的函数,例如为了高亮地块,需要传入地块;并且需要使用全局变量来为函数传入参数,因为鼠标位置的获取需要在其他事件处理器中完成。其他绘制函数都是不带参数的,那么这个难道不能统一吗?而且全局变量还是少用为好,函数式编程连变量都反对,何况全局变量。
让我优雅一次吧。我定义了如下委托变量:
public Func<CityParcel, Action> PaintHoverParcel;
在SetTool方法中为它赋值:
PaintHoverParcel = parcel => () => { if (parcel != null) { Point[] pts = parcel.Domain.Points.Select(x => CanvasCoordinate(x)).ToArray(); g.DrawPolygon(new Pen(Color.Red, 5), pts); } };
在MouseMove事件处理器中,
_display.PaintTool = _display.PaintHoverParcel(_display.DetectParcelHover(e.X, e.Y));
其中DetectParcelHover函数用于返回鼠标位置的地块,传入PaintHoverParcel中,返回正好是一个Action,于是赋值给PaintTool。
为了帮助理解,我做如下解说:为了达成不带参数的绘制地块函数,需要给每个地块做一个单独的绘制函数;但是工作量大,于是用一个函数来实现,那就是一个这样的函数:给定一个地块,返回绘制这个地块的函数。这就是我们讨论的“返回函数的函数”,即高阶函数PaintHoverParcel。
这是我在博客园发表的第一篇文章。写这个随笔只是想呼唤一种精神。我们会发现这样一种现象:在美国,越是从Windows API、MFC、Windows Forms、WPF一路走来的老技术人员,越是为每一次新技术带来的改变而欢呼。同样的事情到了中国,则只有无尽的守旧、抱怨、排斥甚至麻木。园子里时常对LINQ不屑的声音不在少数。我们有足够的理由相信,要想创新,首先需要对新事物有一种宽容,只有宽容对待,才能理性发现、理性领悟、理性升华、理性创造。固步自封、安于现状、畏手畏脚、盲目排新的民族,永远无法以创新者的姿态战胜那些曾经敬仰我们的人。也许,那久违的敬仰已经成为你我每一个人的包袱。