让我开拓眼界的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.
但是这段话我并没有看太懂,大概理解就是能使代码简洁,对效率有提升。
从书上学习到惰性求值有两个主要功能:
- 延迟计算 比如有这样一个函数,他的功能是给列表中的每个元素乘2。现在需要给列表里面的每个元素乘8,代码会这样写:
doubleMe(doubleMe(doubleMe(xs)))
在命令式语言中,列表中的数据会遍历三次,每次乘2。如果利用了惰性求值,代码会在真正需要结果的时候进行计算,在Haskell中,这段代码只会遍历一次列表,就能得到最终的数值。相比而言,少了遍历次数。(只遍历一次这个说法,目前没有验证)。
- 生成无限的数据 如维基百科上给的用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,例如书中介绍的生成一个直角三角形的组合,条件是:
- 三边长度为整数且均小于10
- 周长为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的实例都可以。
有点意思~
最后
笔者也是个初学者,很多概念可能理解的并不准确,在后续的学习中可能会不断的完善。如有误导倍感抱歉。
(完)
- 本文作者:吴泽辉
- 本文链接:https://mutex.top/posts/fb5b0c36/
- 发表日期:2019年5月25日
- 版权声明:本文章为原创,采用《知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议》进行许可