这几天都在看函数式语言。今年以来发现这东西应用的越来越多。
函数式语言,定义有广义和狭义。最简单的概念就是函数是元数据,具有和整数,字符串同等的地位。函数可以做函数的参数,是啊,c语言可以用函数指针来模拟。但是函数式语言里面函数能作为返回值!c里面可不能返回一个函数指针,这个指针指向一个新构建的没有定义过的函数。
在最严格的函数式语言里面,比如haskell,没有变量。赋值语句就是公式定义。x=x+1是绝对不成立的。数组或者列表处理用的是递归。阶乘的函数定义,f( x+1)=x*f(xs),f(1)=1;你看,就是数学定义翻译一下。我们写的程序其实包括两个部分:做什么和如何做。函数式语言就是尽量去掉如何做的描写,只写做什么的部分。
我们现在用的语言里面,除了最古老的lisp以外,python,ruby等新一点的脚本语言基本都支持函数式编程。javascript也支持,而且是现在新一点的框架里面必用的模式。
“將Object為主的程式碼轉回Procedure的方式:將Object化成Record,將Method轉成Function,將this(或self)當作Function的參數,把Function集中放到Module(模組)。將程式中用到迴圈的地方,盡量轉成遞迴(Recursion)。先不要管執行效率的問題。將程式中用到if/else或switch/case的地方,改用Pattern Matching(模式比對)。只要做到上述這三件事,你的F#程式會具有濃濃的FP風味。”
1:速度大家可以看到,函数式语言目前基本都是脚本语言,动态语言,很少有编译类型的。这就带来一个过去函数式语言一直发展不起来的原因:太慢。lisp的一些方言,比如scheme,加上haskell这个比较纯的函数式语言,可以编译为c语言进而变成本地代码。但是现在由于jit(即时编译)技术的发展,这个已经不是问题。最简单的c++比起c来多了很多包装和抽象,但是c++解决常见问题的程序反而比c快。比如java是在虚拟机里面跑的,理论上效率比C低。但是jit可以即时翻译为本地代码,所以在数值计算上可以比c还要高。而lisp在数值计算上如果是某些特殊的编译器可以比c快很多。这是因为C的实现太过古老,要保持兼容性,而很多算法实际上是很低效的但是又不得不用。类型问题和函数副作用的问题又阻止了很多比较激进的编译器优化:你不知道简化会不会造成计算结果和原来不一样,那么你就不敢做这个优化。C++到java因为数据类型检查更严格,面向对象也约束了很多变量的作用范围,所以优化可以更彻底。如果是lisp,因为只定义了做什么,而没有定义怎么做,机器就可以尽情的找最优化的方案。当然,在数值计算方面,数学公式的简化推导已经有成熟理论,所以优化是巨大的,但在其他领域,java和lisp还是比不上c或者c++的。而c的存在也不是没有意义:c很灵活(这也是无法深度优化的原因),c不需要额外的一些运行时支持(c++需要维护一些虚拟函数表格之类)使得c在写操作系统的时候无可替代的作用。而且c++过于复杂,很多特征容易被人滥用。
有个测试结果是这样的,跑一个程序,c++需要的时间是1,那么c在1.05左右,java约1.6,javascript大约是4,编译过的scheme或者haskell小于3-4,go语言居然有5(这可是没有虚拟机的编译语言),lua是2(编译后)到32(编译前),python50左右,php过百,perl64,ruby接近60。
里面值得注意的是javascript在google的v8引擎下居然能跑到仅仅3倍时间,可见jit的威力。而比较晚的语言在设计的时候都考虑了虚拟机和编译的需求,速度并不慢。ruby是个特例,应该是开始的时候并没有考虑到执行效率,而是以写起来方便为主要目标吧,毕竟实现的特征和类型反射做的是最方便的。
可见动态语言是可以将灵活性,严谨的数据类型和速度兼顾的。
2:无关的东西 基础数据类型
脚本语言里面大多实现了列表,map数组(也就是可以用任意数据类型做数组下标)等数据类型。这些数据类型很有必要,一个是到了比较复杂的数据情况,建立这些数据类型几乎是必然的,而系统自己实现因为算法很成熟,比起我们自己临时写一些总要好一些。比如map数组用的哈希表,比起普通数组在时间上都是O(1)的,如果我们自己用结构数组去实现,在进行查找和添加等操作时如果不想麻烦的话,用的肯定是O(n)的算法,反而比系统自己实现的慢。
C++里面实现这些可以用标准泛型库。
这些东西在写程序的时候大大增加了便利,以往很多写起来很麻烦的算法现在写起来就很简单了。而动用函数式编程,写程序就更只管快捷了。比如要对数组进行过滤,两个数组里面的数字逐对叠加,传统需要写一个循环。现在只要用一个map或者filter函数就可以了,具体操作可以写在一个函数内的匿名函数(用lambda表达式)。但是函数式语言基本肯定要用到表,其他就不一定了。
3:函数式编程那么多函数调用,会不会带来大量消耗?
函数式语言里面函数是基本数据,所以很多我们觉得不必用函数的地方也用到了。比如说一个表达式里面有一些类似项,我们想用一个中间变量来简化表达式。函数式编程的办法就是写一个匿名函数,这个函数调用的参数就是前面的中间表达式。这样来避免中间表达式多次求值。而且函数式语言里面可以返回函数。在数学的函数理论里面,每个函数只有一个参量,那么多个参数的表达式怎么办?就是F(a,b)=(f(g(b)))(a),先生成一个使用参量b的g子函数,然后用这个函数制作出函数ff,这个函数ff调用参数a。
比如
func plus(int n){return func(x){return x+n}}
a=plus(3) //a是一个函数
a(5) //返回8
b=plus(6)
b(5) //返回11,我们把plus'(5,6)变成了b(5),也就是(plus(6))(5)
也就是函数实例是带有它产生时候的环境变量的。那么函数调用必然要记住这些东西,比起普通编程里面的函数调用需要更多步骤。但是函数式编程其实是可以通过归纳把不必要的部分去掉的。比如尾递归,如果一个函数递归调用自己,而最后的返回语句就是一个光秃秃的函数调用,而且是调用自己,那么就可以把这个调用转换为goto语句。所以合理的函数式编程并不过分消耗计算资源。
4:函数式计算的副作用
像haskell这样的严格语言里面函数调用是没有副作用的,也就是说只要参数一样,结果肯定一样。lisp大部分时间是没有副作用的,但是允许它存在。没有副作用有好几个很大的用处
a 优化编译的作用 因为不必担心副作用,所以编译优化的时候可以大胆合并类似项,去掉不必要的计算步骤,计算顺序可以随意改变,记忆函数调用求值,像a+f(x+2)+f(2+x)就会自动简化为a+2*f(x+2)等等。像自动展开循环,把数列和直接归纳计算等等都不在话下。
b 函数可以随时停下来,返回,然后需要的时候从断点重新开始。最常见的是yield
func fabbi(){
a,b=1,1
while(1){
yield a
a,b=b,a+b
}}
fabbi() //return 1
fabbi() //return 2
fabbi() //return 3,5,8,11.....
这里违反了同样输入带来同样输出的规定,不过大家可以看到在写一些循环的时候这种写法的威力,如果不用yield基本上不可能把这个函数独立写到循环外面了。其实肯定可以做到,但是远远没有这么直观和简单。
也就是说,无需线程等操作系统支持,函数式语言可以轻易实现模拟多线程。go语言里面就大量使用这种手法。现代很多脚本语言也用来写异步语法。比如我们写界面语言,经常要等待键盘输入,或者等待某个操作完成(比如读盘或者下载)。过去我们这个时候只能写一个无限循环不停等,其他部分的操作就只能等了。现在只要马上返回一个触发函数,这个触发函数保留了当前运行状态然后去干别的事情。等我们要的操作完成的时候,去调用那个触发函数,我们就继续从刚才的断点往下跑了。使用函数式编程的匿名函数还可以把这样一个阻塞式函数自动变成非阻塞式的。传统方法不是不能做到,只是很难在简单写程序的情况下又保证跑下去和当时一样。
现代程序的一个特点是要做大量的并行操作。网页程序同时在刷新界面元素,等待后台读取数据,还在监视用户输入,同时还在播放背景音乐。使用函数式编程可以大大简化。传统这些用的是回调函数,函数式编程可以直接把回调函数写成匿名函数包在调用语句里面,十分直观。更别谈如果要用到外部变量的时候非函数语言如何正确传播变量了。
5 函数式语言的其他
a 延时求值 可以构造无穷列表
b 模糊求值 可以自动搜索匹配
c 抽象计算 可以简化很多算法,而不用多态和泛型
面向对象是数据自带方法,函数式编程是方法自带数据。前者在数据类型基本不变时有着计算简单快速的优势,后者则可以套用到多种数据类型而有编程简单灵活的优势。
函数式语言的抽象性是其主要优点。很多编程技巧都用不上了。有人归纳说一个足够复杂的系统,迟早会发明出一个类似lisp的内部语言,而且还是低效和不完善的。因为系统过于复杂,就必然需要一个抽象层来简化。MS office有一个vba。很多游戏场景用脚本描述。现在连界面都用lua脚本描述。所以觉得脚本语言慢是不必要的,大部分计算不是消耗在脚本,而是消耗在IO绘图数据库等方面。而lisp等抽象语言带来的垃圾收集,列表和map数组,异步操作,自动类型推断等,极大的简化了编程。所以在计算机速度越来越快,计算机比人工便宜得多的时代,使用脚本语言是划算的。而且,现代编译技术在一个良好设计的脚本语言里面,根本就不慢。go语言等甚至根本就不是脚本,直接就是比c编译速度快得多的强类型编译语言(当然是以执行速度为代价,但是go语言目前的编译器还远没有优化)。这样一个简化编写又大大减少潜在bug的语言(类型错误,逻辑错误,指针分配/回收错误),c语言是该从非操作系统的编程语言中退役了。java和c++都属于过度设计+委员会扯皮,所以反而不如c#的快速迭代和清爽(背后的主持人功力也是原因),总之类似go这样的重新触发设计c的编译型语言和新时代代替lisp做日常编程的脚本语言都是很需要的。
新语言的发明在2000年后陷入低潮,但是最近又有抬头的趋势。因为网络编程和新的jit编译理论,以及脚本语言的实践带来了大量新的方法。可见除了c/lisp这一抽象一机器的两个极端,中间的语言都需要大洗牌一次了。c++,java以及大量的脚本语言这些可能除了名字没变,编程风格等可能都会大变样,迎接“简单就是美”的新一轮抽象化。
函数式语言,定义有广义和狭义。最简单的概念就是函数是元数据,具有和整数,字符串同等的地位。函数可以做函数的参数,是啊,c语言可以用函数指针来模拟。但是函数式语言里面函数能作为返回值!c里面可不能返回一个函数指针,这个指针指向一个新构建的没有定义过的函数。
在最严格的函数式语言里面,比如haskell,没有变量。赋值语句就是公式定义。x=x+1是绝对不成立的。数组或者列表处理用的是递归。阶乘的函数定义,f( x+1)=x*f(xs),f(1)=1;你看,就是数学定义翻译一下。我们写的程序其实包括两个部分:做什么和如何做。函数式语言就是尽量去掉如何做的描写,只写做什么的部分。
我们现在用的语言里面,除了最古老的lisp以外,python,ruby等新一点的脚本语言基本都支持函数式编程。javascript也支持,而且是现在新一点的框架里面必用的模式。
“將Object為主的程式碼轉回Procedure的方式:將Object化成Record,將Method轉成Function,將this(或self)當作Function的參數,把Function集中放到Module(模組)。將程式中用到迴圈的地方,盡量轉成遞迴(Recursion)。先不要管執行效率的問題。將程式中用到if/else或switch/case的地方,改用Pattern Matching(模式比對)。只要做到上述這三件事,你的F#程式會具有濃濃的FP風味。”
1:速度大家可以看到,函数式语言目前基本都是脚本语言,动态语言,很少有编译类型的。这就带来一个过去函数式语言一直发展不起来的原因:太慢。lisp的一些方言,比如scheme,加上haskell这个比较纯的函数式语言,可以编译为c语言进而变成本地代码。但是现在由于jit(即时编译)技术的发展,这个已经不是问题。最简单的c++比起c来多了很多包装和抽象,但是c++解决常见问题的程序反而比c快。比如java是在虚拟机里面跑的,理论上效率比C低。但是jit可以即时翻译为本地代码,所以在数值计算上可以比c还要高。而lisp在数值计算上如果是某些特殊的编译器可以比c快很多。这是因为C的实现太过古老,要保持兼容性,而很多算法实际上是很低效的但是又不得不用。类型问题和函数副作用的问题又阻止了很多比较激进的编译器优化:你不知道简化会不会造成计算结果和原来不一样,那么你就不敢做这个优化。C++到java因为数据类型检查更严格,面向对象也约束了很多变量的作用范围,所以优化可以更彻底。如果是lisp,因为只定义了做什么,而没有定义怎么做,机器就可以尽情的找最优化的方案。当然,在数值计算方面,数学公式的简化推导已经有成熟理论,所以优化是巨大的,但在其他领域,java和lisp还是比不上c或者c++的。而c的存在也不是没有意义:c很灵活(这也是无法深度优化的原因),c不需要额外的一些运行时支持(c++需要维护一些虚拟函数表格之类)使得c在写操作系统的时候无可替代的作用。而且c++过于复杂,很多特征容易被人滥用。
有个测试结果是这样的,跑一个程序,c++需要的时间是1,那么c在1.05左右,java约1.6,javascript大约是4,编译过的scheme或者haskell小于3-4,go语言居然有5(这可是没有虚拟机的编译语言),lua是2(编译后)到32(编译前),python50左右,php过百,perl64,ruby接近60。
里面值得注意的是javascript在google的v8引擎下居然能跑到仅仅3倍时间,可见jit的威力。而比较晚的语言在设计的时候都考虑了虚拟机和编译的需求,速度并不慢。ruby是个特例,应该是开始的时候并没有考虑到执行效率,而是以写起来方便为主要目标吧,毕竟实现的特征和类型反射做的是最方便的。
可见动态语言是可以将灵活性,严谨的数据类型和速度兼顾的。
2:无关的东西 基础数据类型
脚本语言里面大多实现了列表,map数组(也就是可以用任意数据类型做数组下标)等数据类型。这些数据类型很有必要,一个是到了比较复杂的数据情况,建立这些数据类型几乎是必然的,而系统自己实现因为算法很成熟,比起我们自己临时写一些总要好一些。比如map数组用的哈希表,比起普通数组在时间上都是O(1)的,如果我们自己用结构数组去实现,在进行查找和添加等操作时如果不想麻烦的话,用的肯定是O(n)的算法,反而比系统自己实现的慢。
C++里面实现这些可以用标准泛型库。
这些东西在写程序的时候大大增加了便利,以往很多写起来很麻烦的算法现在写起来就很简单了。而动用函数式编程,写程序就更只管快捷了。比如要对数组进行过滤,两个数组里面的数字逐对叠加,传统需要写一个循环。现在只要用一个map或者filter函数就可以了,具体操作可以写在一个函数内的匿名函数(用lambda表达式)。但是函数式语言基本肯定要用到表,其他就不一定了。
3:函数式编程那么多函数调用,会不会带来大量消耗?
函数式语言里面函数是基本数据,所以很多我们觉得不必用函数的地方也用到了。比如说一个表达式里面有一些类似项,我们想用一个中间变量来简化表达式。函数式编程的办法就是写一个匿名函数,这个函数调用的参数就是前面的中间表达式。这样来避免中间表达式多次求值。而且函数式语言里面可以返回函数。在数学的函数理论里面,每个函数只有一个参量,那么多个参数的表达式怎么办?就是F(a,b)=(f(g(b)))(a),先生成一个使用参量b的g子函数,然后用这个函数制作出函数ff,这个函数ff调用参数a。
比如
func plus(int n){return func(x){return x+n}}
a=plus(3) //a是一个函数
a(5) //返回8
b=plus(6)
b(5) //返回11,我们把plus'(5,6)变成了b(5),也就是(plus(6))(5)
也就是函数实例是带有它产生时候的环境变量的。那么函数调用必然要记住这些东西,比起普通编程里面的函数调用需要更多步骤。但是函数式编程其实是可以通过归纳把不必要的部分去掉的。比如尾递归,如果一个函数递归调用自己,而最后的返回语句就是一个光秃秃的函数调用,而且是调用自己,那么就可以把这个调用转换为goto语句。所以合理的函数式编程并不过分消耗计算资源。
4:函数式计算的副作用
像haskell这样的严格语言里面函数调用是没有副作用的,也就是说只要参数一样,结果肯定一样。lisp大部分时间是没有副作用的,但是允许它存在。没有副作用有好几个很大的用处
a 优化编译的作用 因为不必担心副作用,所以编译优化的时候可以大胆合并类似项,去掉不必要的计算步骤,计算顺序可以随意改变,记忆函数调用求值,像a+f(x+2)+f(2+x)就会自动简化为a+2*f(x+2)等等。像自动展开循环,把数列和直接归纳计算等等都不在话下。
b 函数可以随时停下来,返回,然后需要的时候从断点重新开始。最常见的是yield
func fabbi(){
a,b=1,1
while(1){
yield a
a,b=b,a+b
}}
fabbi() //return 1
fabbi() //return 2
fabbi() //return 3,5,8,11.....
这里违反了同样输入带来同样输出的规定,不过大家可以看到在写一些循环的时候这种写法的威力,如果不用yield基本上不可能把这个函数独立写到循环外面了。其实肯定可以做到,但是远远没有这么直观和简单。
也就是说,无需线程等操作系统支持,函数式语言可以轻易实现模拟多线程。go语言里面就大量使用这种手法。现代很多脚本语言也用来写异步语法。比如我们写界面语言,经常要等待键盘输入,或者等待某个操作完成(比如读盘或者下载)。过去我们这个时候只能写一个无限循环不停等,其他部分的操作就只能等了。现在只要马上返回一个触发函数,这个触发函数保留了当前运行状态然后去干别的事情。等我们要的操作完成的时候,去调用那个触发函数,我们就继续从刚才的断点往下跑了。使用函数式编程的匿名函数还可以把这样一个阻塞式函数自动变成非阻塞式的。传统方法不是不能做到,只是很难在简单写程序的情况下又保证跑下去和当时一样。
现代程序的一个特点是要做大量的并行操作。网页程序同时在刷新界面元素,等待后台读取数据,还在监视用户输入,同时还在播放背景音乐。使用函数式编程可以大大简化。传统这些用的是回调函数,函数式编程可以直接把回调函数写成匿名函数包在调用语句里面,十分直观。更别谈如果要用到外部变量的时候非函数语言如何正确传播变量了。
5 函数式语言的其他
a 延时求值 可以构造无穷列表
b 模糊求值 可以自动搜索匹配
c 抽象计算 可以简化很多算法,而不用多态和泛型
面向对象是数据自带方法,函数式编程是方法自带数据。前者在数据类型基本不变时有着计算简单快速的优势,后者则可以套用到多种数据类型而有编程简单灵活的优势。
函数式语言的抽象性是其主要优点。很多编程技巧都用不上了。有人归纳说一个足够复杂的系统,迟早会发明出一个类似lisp的内部语言,而且还是低效和不完善的。因为系统过于复杂,就必然需要一个抽象层来简化。MS office有一个vba。很多游戏场景用脚本描述。现在连界面都用lua脚本描述。所以觉得脚本语言慢是不必要的,大部分计算不是消耗在脚本,而是消耗在IO绘图数据库等方面。而lisp等抽象语言带来的垃圾收集,列表和map数组,异步操作,自动类型推断等,极大的简化了编程。所以在计算机速度越来越快,计算机比人工便宜得多的时代,使用脚本语言是划算的。而且,现代编译技术在一个良好设计的脚本语言里面,根本就不慢。go语言等甚至根本就不是脚本,直接就是比c编译速度快得多的强类型编译语言(当然是以执行速度为代价,但是go语言目前的编译器还远没有优化)。这样一个简化编写又大大减少潜在bug的语言(类型错误,逻辑错误,指针分配/回收错误),c语言是该从非操作系统的编程语言中退役了。java和c++都属于过度设计+委员会扯皮,所以反而不如c#的快速迭代和清爽(背后的主持人功力也是原因),总之类似go这样的重新触发设计c的编译型语言和新时代代替lisp做日常编程的脚本语言都是很需要的。
新语言的发明在2000年后陷入低潮,但是最近又有抬头的趋势。因为网络编程和新的jit编译理论,以及脚本语言的实践带来了大量新的方法。可见除了c/lisp这一抽象一机器的两个极端,中间的语言都需要大洗牌一次了。c++,java以及大量的脚本语言这些可能除了名字没变,编程风格等可能都会大变样,迎接“简单就是美”的新一轮抽象化。