读书心得 oCaml 语言和F#和Haskell
http://www.douban.com/note/332000398/ 这篇日记说了lisp语言的一些在推广上面的缺陷。lisp语法的优点同时也是他的缺点,我们可以看到,lisp的优点除了内在的函数式编程,还有语法上面的优点,就在于写可以生成程序的程序,也就是宏。而这又有赖于其高度结构化的程序,也就是繁琐乃至于不容易写不容易读的过多层次和讨厌的前缀表达式。另外一个讨厌的地方就是没有类型系统,虽然有一个struct,某些变种lisp里面也有一些简单的类型限制,别的不说至少除了list之外内置起码也是有string的好不好,但是有不等于够用。
那么有没有保留函数式编程的优点但是又用简练的容易读的语法的编程语言呢?常见的python有点那个意思,ruby用的很多。但是比较正统的还是ML语言系列,最流行的是面向对象的OCaml,最新的是微软在.net语言系列里面的F#,最炫的当然就是haskell了。
ocaml语言的语法除了少数几点,和传统的命令式语言非常相像,而且还有很多的语法糖,非常实用。你如果不熟悉函数式编程,直接把它当作更方便一点的传统语言也是可以的。
ocaml语言和传统语法不同的有几点:
1 尽量不用全局变量,局部值如非特殊声明否则不可变,尽量缩小变量的作用域。这点是传统命令式语言也极力强调的。除了传统的用名字空间和包来隔离同名变量,ml语言可以用
let value1=define expr in (block)
value1的作用范围仅仅在in语句后面的语句块里面。当然,不用in这个关键词的话就是本层语句块有效。(语法块就是c或者java里面用花括号包起来的那个东西,可以是if或者for语句里面也可以是函数定义里面。因为函数式语言里面什么都是函数,所以层次划分就用语法块来区别)
有时候需要用到可变量,如果可变量仅仅作用于局部的话,可以用ref来解决。
open Printf
let a= ref 1
for i=1 to 3 do
a=a!+1
printf "%d \n" a
open相当于java里面的导入包的名字空间。这种情况可以把a=a!+1看做是a2=a1+1,每次生成一个新不变量,只是都叫一个名字而已,毕竟老的不变量以后不可用了。只要没有两个语句块同时使用一个变量,这种用法实际上就是完全不使用可变量的一种语法糖。
那么什么时候会有两个语法块同时使用一个变量?一个是循环语句里面套了两个语法块,一个是几个函数共用变量轮流被调用。解决的方法就是,不要用共享变量来传递消息,而要用传递消息来共享变量。
当然,共享变量等副作用是不可避免的。原因有2.一是这个副作用对应实体,比如记录外部温度,你必然有一个变量,不能外部变了你内部还不变。你要持久化,比如说把变量存到磁盘,你最后还是要一个副作用。二是性能问题,你不可能因为一个巨大的结构仅仅因为内部一个小变量变了就重新拷贝一个结构,特别是当两个进程交换内容的时候(比如输入输出,查询数据库等等)。
这个时候F#就用
let mutable a=1
a <-2
这种方法来使用变量。当然,如非需要不推荐。
2 因为函数是第一级元素,可以作为返回值,可以作为运算的值。定义函数用的是和赋值(或者用函数式编程对不变量的叫法,绑定)一样的语法。
let incr i=i+1 (*语法糖*)
或者内部表达式 let incr =fun i -> i+1
ml语言里面很多东西都有内部和语法糖两种表示方法。
比如列表,内部是 a::b::c::d::[] 的表示,而语法糖方式是[a;b;c;d]。
多参数函数,实际是a->b->c这样的单函数的乘积。
3 匹配式编程
不可变多类型子元素的元组(Tuple,常用于表示具体结构,也就是java里面的struct),不可变尽量单类型元素的列表(List),可变单类型元素的数组(Array)。熟悉json的人就知道这几种的区别。list因为是链表构成,生成新链表的时候可以复用旧链表的内容,所以复制起来特别快,适合大量使用不变量,从而需要经常拷贝大型结构的函数式编程(比如lisp)。
ocaml对于每种复合结构(或者说容器结构)都提供了称为匹配的语法糖,可以用在match with语法和函数定义上。
比如一个元组let v=(a,b)可以用于
match v with
(a,_)-> expr
|(_,c)->expr2
|(_,_)->expr3
可以看到使用分级分隔符的方法可以把lisp那种括号套括号的写法变为一种无需缩进来判断层次的层次结构。函数式语言的一个特点就是一个函数定义几乎总是只用一个语句。
haskell就更进一步,函数的不同参数部分可以分开写。
写list这种递归的结构作为参数的时候更方便了
let fold f li init=function
| [] ->init
| head::tail-> f head (fold f tail init)
let legth unit (dx,dy) =sqrt (dx*.dx+.dy*.dy)*.unit
其中 第二个参数是个结构,使用时被自动拆解。
4 类型
首先,变量可以自动判断类型,从它被赋值的时候。这样的话,就避免了老java里面new一个新泛型对象的时候要在定义和new的时候写两遍类型的事情,对于写函数指针类型,特别是使用了函数指针的函数的指针的类型的时候的麻烦情况了。当然,现在c++也有类型推断了,使用auto关键词。
当变量是一个复合变量的时候,也是如此,比如可以从元组的子项类型推断整体。类型还可以是多形式外加变长,也就是递归的结构。
对于某个变量可能还未获得赋值的情况,也能够正确的用None|Some of int的方式定义出来,然后在函数里面用匹配区分开来。这比带tag的类要更本质和简单一些。特别是对于异常处理,也就是 try with结构,几乎是不可不用的。
5 面向对象
面向对象和函数式编程,本质都是数据加程序。
面向对象,是有类型的数据,加上用来处理自己的数据的程序
函数式编程,使用闭包的形式,让作为返回值的函数戴上闭包里面的数据。
后面可以模拟绝大多数前面的用法(javascript就是如此),前者可以用函数指针(或者说F#里面的委托,也就是带类型和签名的匿名函数指针)完成在分类上少数,实例上大部分的情况下的问题(20%分类沾了80%的数据的二八定律)。
但是两者本质上是不可以互相替代的,后者很显然连前者的一些简单情况都模拟不了,前者可以模拟后者但不光是性能的问题还有类型推导上的一些问题。所以ocaml里面也有原生的面向对象,而不是像某些lisp那样模拟一个。
6 性能
函数式编程很多都属于脚本语言,无法脱离解释器,速度不快。借虚拟机技术和中间编译,很多脚本语言也变得很快,有时候需要语言特有的解释器代码有的不需要。对于可以进行类型推导的语言,比如某些lisp,所有haskell,可以进一步编译为二进制的原生代码。F#生成的是无需语言特定的解释器的虚拟机代码,可以进一步在具体机器上编译为原生代码。
总的来说,达到c一半的速度是可能的。无法进一步提高是因为有闭包的情况下,函数调用不光是把返回地址压栈跳转就完了,还要在堆上保留闭包数据。
为了达到更好的性能,函数式编程应该尽量把递归写成尾递归。具体方式很简单,把要在语句块里面传递的中间变量,变成函数参数的一部分就可以了。如果是带分支的递归,就要用延续函数作为参数了,这样可以通过闭包带更复杂更多的中间变量。
一旦变成了尾递归,函数调用就变成了简单的跳转语句。(当然,别忘记闭包的分配和释放)
那么有没有保留函数式编程的优点但是又用简练的容易读的语法的编程语言呢?常见的python有点那个意思,ruby用的很多。但是比较正统的还是ML语言系列,最流行的是面向对象的OCaml,最新的是微软在.net语言系列里面的F#,最炫的当然就是haskell了。
ocaml语言的语法除了少数几点,和传统的命令式语言非常相像,而且还有很多的语法糖,非常实用。你如果不熟悉函数式编程,直接把它当作更方便一点的传统语言也是可以的。
ocaml语言和传统语法不同的有几点:
1 尽量不用全局变量,局部值如非特殊声明否则不可变,尽量缩小变量的作用域。这点是传统命令式语言也极力强调的。除了传统的用名字空间和包来隔离同名变量,ml语言可以用
let value1=define expr in (block)
value1的作用范围仅仅在in语句后面的语句块里面。当然,不用in这个关键词的话就是本层语句块有效。(语法块就是c或者java里面用花括号包起来的那个东西,可以是if或者for语句里面也可以是函数定义里面。因为函数式语言里面什么都是函数,所以层次划分就用语法块来区别)
有时候需要用到可变量,如果可变量仅仅作用于局部的话,可以用ref来解决。
open Printf
let a= ref 1
for i=1 to 3 do
a=a!+1
printf "%d \n" a
open相当于java里面的导入包的名字空间。这种情况可以把a=a!+1看做是a2=a1+1,每次生成一个新不变量,只是都叫一个名字而已,毕竟老的不变量以后不可用了。只要没有两个语句块同时使用一个变量,这种用法实际上就是完全不使用可变量的一种语法糖。
那么什么时候会有两个语法块同时使用一个变量?一个是循环语句里面套了两个语法块,一个是几个函数共用变量轮流被调用。解决的方法就是,不要用共享变量来传递消息,而要用传递消息来共享变量。
当然,共享变量等副作用是不可避免的。原因有2.一是这个副作用对应实体,比如记录外部温度,你必然有一个变量,不能外部变了你内部还不变。你要持久化,比如说把变量存到磁盘,你最后还是要一个副作用。二是性能问题,你不可能因为一个巨大的结构仅仅因为内部一个小变量变了就重新拷贝一个结构,特别是当两个进程交换内容的时候(比如输入输出,查询数据库等等)。
这个时候F#就用
let mutable a=1
a <-2
这种方法来使用变量。当然,如非需要不推荐。
2 因为函数是第一级元素,可以作为返回值,可以作为运算的值。定义函数用的是和赋值(或者用函数式编程对不变量的叫法,绑定)一样的语法。
let incr i=i+1 (*语法糖*)
或者内部表达式 let incr =fun i -> i+1
ml语言里面很多东西都有内部和语法糖两种表示方法。
比如列表,内部是 a::b::c::d::[] 的表示,而语法糖方式是[a;b;c;d]。
多参数函数,实际是a->b->c这样的单函数的乘积。
3 匹配式编程
不可变多类型子元素的元组(Tuple,常用于表示具体结构,也就是java里面的struct),不可变尽量单类型元素的列表(List),可变单类型元素的数组(Array)。熟悉json的人就知道这几种的区别。list因为是链表构成,生成新链表的时候可以复用旧链表的内容,所以复制起来特别快,适合大量使用不变量,从而需要经常拷贝大型结构的函数式编程(比如lisp)。
ocaml对于每种复合结构(或者说容器结构)都提供了称为匹配的语法糖,可以用在match with语法和函数定义上。
比如一个元组let v=(a,b)可以用于
match v with
(a,_)-> expr
|(_,c)->expr2
|(_,_)->expr3
可以看到使用分级分隔符的方法可以把lisp那种括号套括号的写法变为一种无需缩进来判断层次的层次结构。函数式语言的一个特点就是一个函数定义几乎总是只用一个语句。
haskell就更进一步,函数的不同参数部分可以分开写。
写list这种递归的结构作为参数的时候更方便了
let fold f li init=function
| [] ->init
| head::tail-> f head (fold f tail init)
let legth unit (dx,dy) =sqrt (dx*.dx+.dy*.dy)*.unit
其中 第二个参数是个结构,使用时被自动拆解。
4 类型
首先,变量可以自动判断类型,从它被赋值的时候。这样的话,就避免了老java里面new一个新泛型对象的时候要在定义和new的时候写两遍类型的事情,对于写函数指针类型,特别是使用了函数指针的函数的指针的类型的时候的麻烦情况了。当然,现在c++也有类型推断了,使用auto关键词。
当变量是一个复合变量的时候,也是如此,比如可以从元组的子项类型推断整体。类型还可以是多形式外加变长,也就是递归的结构。
对于某个变量可能还未获得赋值的情况,也能够正确的用None|Some of int的方式定义出来,然后在函数里面用匹配区分开来。这比带tag的类要更本质和简单一些。特别是对于异常处理,也就是 try with结构,几乎是不可不用的。
5 面向对象
面向对象和函数式编程,本质都是数据加程序。
面向对象,是有类型的数据,加上用来处理自己的数据的程序
函数式编程,使用闭包的形式,让作为返回值的函数戴上闭包里面的数据。
后面可以模拟绝大多数前面的用法(javascript就是如此),前者可以用函数指针(或者说F#里面的委托,也就是带类型和签名的匿名函数指针)完成在分类上少数,实例上大部分的情况下的问题(20%分类沾了80%的数据的二八定律)。
但是两者本质上是不可以互相替代的,后者很显然连前者的一些简单情况都模拟不了,前者可以模拟后者但不光是性能的问题还有类型推导上的一些问题。所以ocaml里面也有原生的面向对象,而不是像某些lisp那样模拟一个。
6 性能
函数式编程很多都属于脚本语言,无法脱离解释器,速度不快。借虚拟机技术和中间编译,很多脚本语言也变得很快,有时候需要语言特有的解释器代码有的不需要。对于可以进行类型推导的语言,比如某些lisp,所有haskell,可以进一步编译为二进制的原生代码。F#生成的是无需语言特定的解释器的虚拟机代码,可以进一步在具体机器上编译为原生代码。
总的来说,达到c一半的速度是可能的。无法进一步提高是因为有闭包的情况下,函数调用不光是把返回地址压栈跳转就完了,还要在堆上保留闭包数据。
为了达到更好的性能,函数式编程应该尽量把递归写成尾递归。具体方式很简单,把要在语句块里面传递的中间变量,变成函数参数的一部分就可以了。如果是带分支的递归,就要用延续函数作为参数了,这样可以通过闭包带更复杂更多的中间变量。
一旦变成了尾递归,函数调用就变成了简单的跳转语句。(当然,别忘记闭包的分配和释放)