Haskell是个神码语言

2013年3月3日

Haskell语言是一种函数式编程语言,而且是具有lazy evaluation(懒求值)特性的函数式编程语言。一开始看见函数式编程概念,感觉很高深,很computer science,很有计算机科学范儿,但是,学了一点之后,才发现它偏数学味儿,不是十分贴近computer science,特别对于时间、空间复杂度一开始都是隐藏着的,不作为主要的特性传达给读者的。

我用两本Haskell书来入门,但到目前为止,只能算是入了扇大门,能编些简单的函数而已。这两本书分别是Real World Haskell和Learn You A Haskell For Great Good。前面这本书我读完了前3章,读到第4章半当中的时候实在读不下去了。后面这本书则目前看到Recursion这块,刚讲了快排算法。

Haskell语言的语法里面,函数调用是比较简单易懂的。比如,div 3 2就是对3作除以2的整数除法。第一个div就是函数,后面跟着的都是参数。参数可以用括号括起来,这样就能表达较为复杂的情况,例如嵌套的函数调用:div (add 1 2) 2。

然后复杂的一点的是类型,类型除了对象或数据本身的类型之外,还有type class(类型类属)。比如说,Int类型是整数本身的类型,而Ord(可作大小比较)则是类型类属(type class),类似于C#语言中的接口概念。一个Int一定是一个Ord,一个Ord未必是一个Int。

还有一种东西叫类型变量,其实就是把本来用于数据的变量概念用在类型上面。

为子把上面讲的三个概念放在一起看,我们看一下下面这个函数(效率并不高,时间复杂度O(n ** 2),只是作个例子):

lastSeveral :: Int -> [a] -> [a]
lastSeveral n [] = []
lastSeveral n (x:xs)
    | n < length (x:xs) = lastSeveral n xs
    | otherwise         = x:(lastSeveral n xs)
 
> lastSeveral 1 []
[]
> lastSeveral 1 "abc"
"c"
> lastSeveral 2 "abc"
"bc"

其中,lastSeveral是个函数,它的输入是一个整数Int和一个列表[a],其元素类型用类型变量a表示,但是我们不限定a到底属于哪些type class,所以任意类型的列表都能支持。如果换成这样:lastSeveral :: (Ord a) => Int -> [a] -> [a],那么a就必须是一个拥有Ord特性的类型。它的输出是另一个列表,其中的类型也还是a,也就是说,输入的如果是个整数列表,输出的也还得是个整数列表。

类型声明的下面,我们定义了lastSeveral的实现。具体实现这里谈起来有些长,至于时间复杂度的问题也是因为Haskell列表的实现特性所决定的(是个单链表),所以不详谈。相信看了书以后就能理解它的实现,而做了实验以后就能大致了解它的时间复杂度。

有趣的是,Haskell实现某些算法显得特别简单明了:

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

一个快速排序能只用数行代码就写出来。但是,反过来,有些在常见的命令式(imperative)编程语言(也就是绝大多数编程语言,包括C#、C/C++、Java、Perl、Python、PHP、Ruby、VB、VB.Net、JavaScript、ABAP等等)中,一些简单的程序用Haskell写出来却很晦涩。例如将一个数组中的元素全部随机排列,或者叫“洗牌”[shuffle],是这样的:

import Control.Monad
import Control.Monad.ST
import Control.Monad.Random
import System.Random
import Data.Array.ST
import GHC.Arr
 
shuffle :: RandomGen g => [a] -> Rand g [a]
shuffle xs = do
    let l = length xs
    rands <- take l `fmap` getRandomRs (0, l-1)
    let ar = runSTArray $ do
        ar <- thawSTArray $ listArray (0, l-1) xs
        forM_ (zip [0..(l-1)] rands) $ \(i, j) -> do
            vi <- readSTArray ar i
            vj <- readSTArray ar j
            writeSTArray ar j vi
            writeSTArray ar i vj
        return ar
    return (elems ar)
 
*Main> evalRandIO (shuffle [1..10])
[6,5,1,7,10,4,9,2,8,3]

这个摘自http://www.haskell.org/haskellwiki/Random_shuffle的程序,利用了一些库中的机制来实现shuffle,包括把xs这个链表转换成数组listArray,然后用thawSTArray、readSTArray和writeSTArray来操作数组中的数据。这个过程看上去类似于命令式编程语言而不像函数式编程。简洁性也不是特别好。

总之,感觉并不是每个函数式编程的程序都容易理解,也不是每个都很简洁。我目前认为,函数式编程可能对某些程序来说是可以写得比命令式编程更偷懒,但不是所有程序都行。作为程序员来说,还是在恰当的场合下,把函数式编程的思想用在命令式编程语言中,才会产生较好的效果。需要注意的是Haskell的递归嵌套层数几乎没有限制,但大多数编程语言是有限制的,因此还是不能完全照搬套用。

留下您的评论