函数式编程实践(1):高阶函数的使用

函数式编程与我们的距离并不遥远。虽然大部分人不会选择去学习F#,但是函数式编程的思想可以用C#来实践。自从C#3.0引入了Lambda表达式,虽然是作为LINQ的配角,但是其带来的改变远远超出LINQ的范围,C#从此一只脚迈入了FP的领地。

回想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不屑的声音不在少数。我们有足够的理由相信,要想创新,首先需要对新事物有一种宽容,只有宽容对待,才能理性发现、理性领悟、理性升华、理性创造。固步自封、安于现状、畏手畏脚、盲目排新的民族,永远无法以创新者的姿态战胜那些曾经敬仰我们的人。也许,那久违的敬仰已经成为你我每一个人的包袱。

posted @ 2011-02-14 22:32  乱世虾  阅读(580)  评论(3编辑  收藏