一个问题
假设现在我们需要开发一个绘制数学函数平面图像(一元)的工具库,可以提供绘制各种函数图形的功能,比如直线f(x)=ax+b、抛物线 f(x)=ax²+bx+c或者三角函数f(x)=asinx+b等等。那么怎么设计公开接口呢?由于每种行数的系数(a、b、c等)不同,并且函数构造 也不同。正常情况下我们很难提供一个统一的接口。所以会出现类似下面这样的公开方法:
- //绘制直线函数图像
- public void DrawLine(double a, double b)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,a*x+b);
- points.Add(p);
- }
- //将points点连接起来
- }
- //绘制抛物线图像
- public void DrawParabola(double a, double b, double c)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,a*Math.Pow(x,2) + b*x + c);
- points.Add(p);
- }
- //将points点连接起来
- }
- ...
- DrawLine(3, 4); //绘制直线
- DrawParabola(1, 2, 3); //绘制抛物线
如果像上面这种方式着手的话,绘制N种不同函数就需要定义N个接口。很明显不可能这样去做。
(注,如果采用虚方法的方式,要绘制N种不同函数图像就需要定义N个类,每个类中都需要重写生成points的算法)
如果我们换一种方式去思考,既然是给函数绘制图像,为什么要将它们的系数作为参数传递而不直接将函数作为参数传给接口呢?是的,没错,要绘制什么函 数图像,那么我们直接将该函数作为参数传递给接口。由于C#中委托就是对方法(函数,这里姑且不讨论两者的区别)的一个封装,那么C#中使用委托实现如 下:
- public delegate double Function2BeDrawed(double x);
- //绘制函数图像
- public void DrawFunction(Function2BeDrawed func)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,func(x));
- points.Add(p);
- }
- //将points点连接起来
- }
- ...
- Function2BeDrawed func =
- (Function2BeDrawed)((x) => { return 3*x + 4;}); //创建直线函数
- DrawFunction(func); //绘制系数为3、4的直线
- Function2BeDrawed func2 =
- (Function2BeDrawed)((x) => {return 1*Math.Pow(x,2) + 2*x + 3;}); //创建抛物线函数
- DrawFunction(func2); //绘制系数为1、2、3的抛物线
- Function2BeDrawed func3 =
- (Function2BeDrawed)((x) => {return 3*Math.Sin(x) + 4;}); //创建正弦函数
- DrawFunction(func3); //绘制系数为3、4的正弦函数图像
如上。将函数(委托封装)作为参数直接传递给接口,那么接口就可以统一。至于到底绘制的是什么函数,完全由我们在接口外部自己确定。
将函数看作和普通类型一样,可以对它赋值、存储、作为参数传递甚至作为返回值返回,这种思想是函数式编程中最重要的宗旨之一。
注:上面代码中,如果觉得创建委托对象的代码比较繁杂,我们可以自己再定义一个函数接收a、b两个参数,返回一个直线函数,这样一来,创建委托的代码就不用重复编写。
函数式编程中的函数
在函数式编程中,我们将函数也当作一种类型,和其他普通类型(int,string)一样,函数类型可以赋值、存储、作为参数传递甚至可以作为另外一个函数的返回值。下面分别以C#和F#为例简要说明:
注:F#是.NET平台中的一种以函数式编程范式为侧重点的编程语言。举例中的代码非常简单,没学过F#的人也能轻松看懂。F#入门看这里:MSDN
定义:
在C#中,我们定义一个整型变量如下:
int x = 1;
在F#中,我们定义一个函数如下:
let func x y = x + y
赋值:
在C#中,我们将一个整型变量赋值给另外一个变量:
- int x = 1;
- int y = x;
在F#中,我们照样可以将函数赋值给一个变量:
- let func = fun x y -> x + y //lambda表达式
- let func2 = func
存储:
在C#中,我们可以将整型变量存储在数组中:
int[] ints = new int[]{1, 2, 3, 4, 5};
在F#中,我们照样可以类似的存储函数:
- let func x = x + 1
- let func2 x = x * x
- let func3 = fun x -> x – 1 //lambda表达式
- let funcs = [func; func2; func3] //存入列表,注意存入列表的函数签名要一致
传参:
在C#中将整型数值作为参数传递给函数:
- void func(int a, int b)
- {
- //
- }
- func(1, 2);
在F#中将函数作为参数传递给另外一个函数:
- let func x = x * x //定义函数func
- let func2 f x = //定义函数func2 ***个参数是一个函数
- f x
- func2 func 100 //将func和100作为参数 调用func2
作为返回值:
在C#中,一个函数返回一个整型:
- int func(int x)
- {
- return x + 100;
- }
- int result = func(1); //result为101
在F#中,一个函数返回另外一个函数:
let func x =
let func2 = fun y -> x + y
func2 //将函数func2作为返回值
let result = (func 100) 1 //result为101,括号可以去掉
数学和函数式编程
函数式编程由Lambda演算得来,因此它与我们学过的数学非常类似。在学习函数式编程之前,我们***忘记之前头脑中的一些编程思想(如学习C C++的时候),因为前后两个编程思维完全不同。下面分别举例来说明函数式编程中的一些概念和数学中对应概念关系:
注:关于函数式编程的特性(features)网上总结有很多,可以在这篇博客中看到。
1.函数定义
数学中要求函数必须有自变量和因变量,所以在函数式编程中,每个函数必须有输入参数和返回值。你可以看到F#中的函数不需要显示地使用关键字 return去返回某个值。所以,那些只有输入参数没有返回值、只有返回值没有输入参数或者两者都没有的函数在纯函数式编程中是不存在的。
2.无副作用
数学中对函数的定义有:对于确定的自变量,有且仅有一个因变量与之对应。言外之意就是,只要输入不变,那么输出一定固定不变。函数式编程中的函数也符合该规律,函数的执行既不影响外界也不会被外界影响,只要参数不变,返回值一定不变。
3.柯里化
函数式编程中,可以将包含了多个参数的函数转换成多个包含一个参数的函数。比如对于下面的函数:
- let func x y = x + y
- let result = func 1 2 //result为3
- 可以转换成
- let func x =
- let func2 = fun y -> x + y
- func2
- let result = (func 1) 2 //result结果也为3,可以去掉括号
可以看到,一个包含两个参数的函数经过转换,变成了只包含一个参数的函数,并且该函数返回另外一个接收一个参数的函数。***调用结果不变。这样做的好处便是:讲一个复杂的函数可以分解成多个简单函数,并且函数调用时可以逐步进行。
其实同理,在数学中也有类似“柯里化”的东西。当我们计算f(x,y) = x + y这个函数时,我们可以先将x=1带入函数,得到的结果为f(1,y) = 1 + y。这个结果显然是一个关于y的函数,之后我们再将y=2带入得到的函数中,结果为f(1,2) = 1 + 2。这个分步计算的过程其实就是类似于函数式编程中的“柯里化”。
4.不可变性
数学中我们用符号去表示一个值或者表达式,比如“令x=1”,那么x就代表1,之后不能再改变。同理,在纯函数式编程中,不存在“变量”的概念,也没有“赋值”这一说,所有我们之前称之为“变量”的东西都是标识符,它仅仅是一个符号,让它表示一个东西之后不能再改变了。
5.高阶函数
在函数式编程中,将参数为函数、或者返回值为函数的这类函数统称之为“高阶函数”,前面已经举过这样的例子。在数学中,对一个函数求导函数的过程,其实就是高阶函数,原函数经过求导变换后,得到导函数,那么原函数便是输入参数,导函数便是返回值。
混合式编程风格
过程式、面向对象再到这篇文章讲到的函数式等,这些都是不同地编程范式。每种范式都有自己的主导编程思想,也就是对待同一个问题思考方式都会不同。很明显,学会多种范式的编程语言对我们思维方式有非常大的好处。
无论是本文中举例使用到的F#还是Java平台中的Scala,大多数冠名“函数式编程语言”的计算机语言都并不是纯函数式语言,而是以“函数式” 为侧重点,同时兼顾其他编程范式。就连曾经主打“面向对象”的C#和Java,现如今也慢慢引入了“函数式编程风格”。C#中的委托、匿名方法以及 lambda表达式等等这些,都让我们在C#中进行函数式编程成为可能。如果需要遍历集合找出符合条件的对象,我们以前这样去做:
- foreach(Person p in list)
- {
- if(p.Age > 25)
- {
- //...
- }
- }
现在可以这样:
list.Where(p => p.Age>25).Select(p => p.Name).toArray();
本篇文章开头提出的问题,采用C#委托的方式去解决,其实本质上也是函数式思想。由于C#必须遵循OO准则,所以引入委托帮助我们像函数式编程那样去操作每个函数(方法)。
本篇文章介绍有限,并没有充分说明函数式编程的优点,比如它的不可变特性无副作用等有利于并行运算、表达方式更利于人的思维等等。实质上博主本人并没有参与过实际的采用函数式语言开发的项目,但是博主认为函数式思想值得我们每个人去了解、掌握。