最近因为写node js,开始有机会接触js的函数式写法。关于函数式语言,其实久闻其名,但只是大概了解过一些概念罢了。刚开始听到这个概念觉得不会就是面向过程编程的改良版吧?(自己还是太无知了…)
由于自己的编程语言主要是java,受面向对象语言的影响很深,也一度以为语言的进化已经到了尽头。将来的语言只是更容易写,提供更多的语法糖。导致自己对函数式语言并没太大的关注。于是写代码的时候各种被批,各种0分…
一个老朽的面向对象语言的使用者终于还意识到学习一下函数式语言。当然对很多东西还一知半解,也作为自己的学习笔记,在这里谈谈函数式语言,也说一下如何让自己去“函数式”地思考。
函数式编程
关于函数-1
于函数式编程的对比对象叫做命令式编程(imperative programming)。函数式编程与之的一个重大区别在于函数在语言中的地位。
在命令式编程语言中,函数是处理变量的特殊存在。变量是可以被传来传去的,并当作返回值返回。而函数是做不到的。(当然也有函数指针这种比较特殊的东西)
class Dog {
void bite(Bone bone){
//
}
}
Dog dog = new Dog();
Object a = dog.bite; // <-- 无法把函数作为一个变量传递。
而在函数式语言当中,函数相对于变量就没有那么特别,它们也可以被传来传去,当作返回值返回。
这样做有什么优点?
说说自己的理解。从于面向对象语言的角度来说,因为函数可以不依附于类,还可以当作变量,很直观的优点是代码会变得很简洁。
用一个定义回调函数的类的例子来说明一下。
public class SomeWork {
public void doWork(Callback afterWorkDone){
// do something
cb.do();
}
}
Interface Callback {
void run();
}
实际调用时。
SomeWork someWork = new SomeWork();
someWork.doWork(new Callback() {
@Override
public void run(){
// things to do after work is done.
// 匿名类好烦人...
});
上面的代码,大致是我们为了回调处理定义了专门的接口,然后在调用时创建实现接口匿名类。
那么如果是函数式语言的话,会怎样呢?下面是伪代码,伪代码哦!
doWork(Func afterWorkDone){
// do something
func.apply();
}
Func func = () -> { /* do something */};
SomeWork someWork = new SomeWork();
someWork.doWork(func);
我们可以不用再特意定义一个接口,也不用写丑陋的匿名类了(如果是函数式语言)。
java中现在也引入了函数式语言的特性,也以使用lambda了,可惜因为java自身语言限制,目前还是需要定义Callback这个接口,才能用lambda来写的。
这里稍微扯远一点,类似于Callback这样的类,其实真的因为面向对象语言的限制而被创造出的一种类。其实往往我们需要的就是一个回掉处理,而在面向对象的语言中,我们被迫要胡邹出一个类,然后定义一个行为,还要让人实现这个方法。为了一个回调处理,我们还得定义一个类,还得帮它想个名字,这些东西在使用函数式语言后就显得比较冗余。
关于函数-2
刚才说过,函数可以当作变量。变量也可以通过运算产生新的变量,也就是新的函数。
一般的语言并不提供函数组合的语法,所以还是使用一下伪代码。
假设a -> a,表示一个函数。它获得一个a类型的参数,返回一个类型的返回值。
func1 = a -> a
func2 = a -> a
func3 = func1 $ func2 // $表示调用函数。func3也是一种a -> a的函数。
上面说的是func1是一种函数。接受a类型,返回a类型。
func2也是接受a类型,返回a类型。
那func1是可以接受func2的运算结果吧,那把它们连一块吧。
func2接受a,返回a,然后把这个a交给func1,func1也返回a。这一整个过程也成了一个接受a返回a的函数,叫它func3
如果语言上对这种组合有好的支持,我们就可以把函数给串起来,像下面这样。
funcA $ funcB $ funcC
比如说关于组装自行车的函数。它接受一个Bike类,返回一个Bike类。 Bike -> Bike
addWheel = Bike -> Bike // 没有写实现
addChain = Bike -> Bike // 没有写实现
addLamp = Bike -> Bike // 没有写实现
constructBike = addWheel $ addWheel $ addChain $ addLamp // 还是Bike -> Bike函数
constructOneWheeledBike = addWheel $ addChain $ addLamp // 还是Bike -> Bike函数
我们把函数组合后,有了组装二轮自行车的函数和独轮车的函数。
当然你可能觉得命令式语言也做得到啊。
addWheel(addChain(addLamp(bike)));
当请注意,命令式语言中,函数是特殊的东西,所以当你对函数进行组合是,你必须生名一个新函数。
public Bike constructBike(Bike bike){
return addWheel(addChain(addLamp(bike)));
}
public Bike constructOneWheeledBike(Bike bike){
return addWheel(addChain(addLamp(bike)));
}
而且上面的做法需要调用的嵌套。写多了很难读。极端一点
aMethod(bMethod(cMethod(dMethod(eMethod()))));
而函数式的写法会变成像一个链条。
aFunc()
.bFunc()
.cFunc()
.dFunc()
.eFunc()
或者
aFunc $ bFunc $ cFunc $ eFunc
你可能觉得这只是一个语法糖罢了。但这蕴含着函数式语言的一种思想,把小单位简单的处理组合起来,可以变成一个能应对复杂问题的处理。
当然面向对象也有这种思路,只不过面向对象语言中,这种简单的处理是由类来承担,或者类的方法,我们将类的各种方法组合调用来构建一个程序。而函数式编程中,这种简单处理的元素变成了函数。
关于副作用
另一个关于函数式编程的关键字便是副作用。当一个函数的处理改变了外部的任何状态那他就产生了副作用。
void prank(List<Integer> nums) {
nums.add(1); // 改变了参数的状态,产生了副作用
}
对于nums这个list的改变,会影响到应用这个nums的处理。这种隐性的状态改变容易造成程序问题。那要写成没有副作用的处理,大概是下面这个样子。
List<Integer> addElement(List<Integer> nums){
List<Integer> newList = new List<>();
newList.addAll(nums);
newList.add(1);
return newList;
}
这样写还麻烦。而函数式语言会对变量的不可变性(immutability)有很好的支持。比如在函数式语言中
nums.add(1);
这个调用会生成新的list,nums的状态不发生改变。
另外诸如IO操作,Http请求等都属于副作用。函数式编程对于副作用的限制也各不相同。如Haskell就完全不允许副作用。而javascript,在语言语法上对副作用没有限制。
关于函数-3
我们假定我们用了一种严格的函数式语言,无法产生任何副作用。那下面的函数就是没有意义的了。
public class Example {
public void doSomething(Job job){
// whatever you want to do with job.
}
}
Example ex = new Example();
ex.doSomething(job1);
假设doSomething中对job的操作,改变了Job的状态,但因为语言不支持/不允许副作用,这个状态不会反映到参数的传递方的job1。而这个函数又没有返回值,本质上他对程序没有任何的影响。所以在函数式语言中,函数可定是有返回值的。(没有返回值,函数还不能进行组合呢~)
总结
这次谈了一下函数式语言中的函数。但目前很多的开发环境比较多的可能还是使用有函数式特性的命令式语言吧(比如java8)。那在使用这种语言时,如何能写”函数式“的程序呢。我给了自己以下建议逼迫自己来写程序。这样会用新的思想去思考程序该怎么写。
- 尽量不写返回值为void的方法
- 尽量不要写有副作用的方法
- 尽量不写for循环
不使用for循环?如果大家使用过函数式语言,这个好像是个很自然的选择。这个话题下次再展开吧。