让我开拓眼界的Haskell

最近想学习一门函数式编程语言,选择了Haskell。教程使用的是HASKELL 趣学指南的电子书,链接是在线版本。

如《Haskell趣学指南》译者所言,学习Haskell的理由是开拓眼界、有趣,这就够了。

注:本文所写的内容局限于笔者的知识储备,在其他人看来可能并不新鲜,所以仁者见仁了:)。

下面简单罗列下学习到目前为止,让我感到很惊讶的几个点:

变量

在函数式编程语言中,一旦变量被赋值,就不能再变了。这点跟命令式语言完全不一样,在命令式语言中经常会根据上下文来给同一个变量赋不同的值。

惰性求值

关于惰性求值,官网是这样介绍的:

Functions don’t evaluate their arguments. This means that programs can compose together very well, with the ability to write control constructs (such as if/else) just by writing normal functions. The purity of Haskell code makes it easy to fuse chains of functions together, allowing for performance benefits.

但是这段话我并没有看太懂,大概理解就是能使代码简洁,对效率有提升。

从书上学习到惰性求值有两个主要功能:

  1. 延迟计算 比如有这样一个函数,他的功能是给列表中的每个元素乘2。现在需要给列表里面的每个元素乘8,代码会这样写:
doubleMe(doubleMe(doubleMe(xs)))

在命令式语言中,列表中的数据会遍历三次,每次乘2。如果利用了惰性求值,代码会在真正需要结果的时候进行计算,在Haskell中,这段代码只会遍历一次列表,就能得到最终的数值。相比而言,少了遍历次数。(只遍历一次这个说法,目前没有验证)。

  1. 生成无限的数据 如维基百科上给的用Haskell生成斐波纳契数列的例子:
fids = 0 : 1 : zipWith (+) fids (tail fids)

这段代码没有退出条件,如果放在命令式语言中,最终会导致内存用尽而崩溃,但在惰性求值的特性下,可以很容易的生成任意长度的斐波纳挈的列表,例如,取10个数据:

take 10 fids

-- 输出

-- [0,1,1,2,3,5,8,13,21,34]

haskell中的函数式引用透明的,也即参数不变,函数的结果也不会变。惰性求值也是依赖于这一点,所以在什么时候计算无关紧要。

类型与typeclass

Haskell是静态类型语言,编译器需要在编译的时候知道变量的类型。但是跟c这种又不一样,在定义变量的时候也可以不指定类型,因为haskell可以做类型推倒。例如在ghci下测试,用:type查看变量类型:

ghci>a = 10
ghci>:type a
a :: Num t => t

可以看到解释器识别出来变量a是Num类型。也可以通过下面的方式显示的指定类型:

ghci>a = 10 :: Int
ghci>:type a
a :: Int

typeclass

现在主流的编程范式仍然是oop,个人理解haskell的typeclass跟oop有点像。

用最简单的Eq类来说明,定义一个函数,比较两个参数是否相等,并返回一个Bool类型。

myEq x y = x == y

在ghci中用:type查看函数的类型,输出如下:

myEq :: Eq a => a -> a -> Bool

可以看出ghci分析出我们的函数接收两个Eq类的实例,并返回一个布尔值。用oop的语言讲就是:两个参数需要实现Eq类判断相等的方法,才能进行比较。

这里的Eq就像php中的interface。

除了Eq之外还有很多typeclass,原理都是一样的,如果某个类型(例如上面的参数a)是typeclass的实例,那么必须实现typeclass的方法。

列表推倒式

直接上demo,例如书中介绍的生成一个直角三角形的组合,条件是:

  1. 三边长度为整数且均小于10
  2. 周长为24

用列表推倒式实现为:

-- a为斜边

[(a, b, c) | a <- [1..10], b <- [1..a], c <- [1..b], a^2 == b^2 + c^2, a + b + c == 24]

-- 输出

-- [(10,8,6)]

代码是不是非常简洁易懂呢?

递归

haskell列表有个语法糖,即:1:2:[] == [1, 2],这使得haskell的递归写起来很简单易懂:

例如实现获取列表最大值:

maximum' :: (Ord a) => [a] -> a
maximum' [] = error "list can't empty"
maximum' [x] = x
maximum' (x:xs) = max x (maximum' xs)

或者实现快排:

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

在haskell中写递归,给人一种说不出来很舒服的感觉,例如快排,看到代码就是很直接。

难道这就是知乎答主说的,我还理解不太好。

函数式编程关心数据的映射,命令式编程关心解决问题的步骤

柯里函数

在命令式编程语言中,函数的参数是固定,带有默认值的参数本质上也一样。但是在haskell中,函数默认都是柯里化的,也就是函数不会一次性取完所有的参数,而是每次取一个参数,并返回一个一元函数来取下一个参数,以此类推。

例如max函数,它接收两个参数,返回大的那个,类型声明如下:

ghci>:t max
max :: Ord a => a -> a -> a

可以这样调用:

max 10 20

然而也可以这样调用:

(max 10) 20

这里的(max 10)返回的一个一元函数(也是一个部分应用的函数),接收一个参数,并返回跟10相比较大的那个。

所以我们可以这样声明一个函数:

let maxTen = max 10

调用的时候,这两者是等价的:

ghci>maxTen 20
ghci>max 10 20

haskell的类型声明是右结合的,所以max的类型声明也可以写成这样:

max :: (Ord a) => a -> (a -> a)

这样来读:max函数接收a参数,返回一个函数,这个函数接收a参数并且返回值也是a。

其中a是类型变量,任何Ord typeclass的实例都可以。

有点意思~

最后

笔者也是个初学者,很多概念可能理解的并不准确,在后续的学习中可能会不断的完善。如有误导倍感抱歉。

(完)