为什么要学习函数式编程? 为什么要学习 Haskell?
.net 到前端, C# 和 JavaScript 对我来说如果谈不上精通, 最起码也算是到了非常熟悉的程度. 这两门语言就像是我的盾牌和宝剑, 给我保驾护航, 开山劈石, 伴随着我不断成长. 同时 C# 和 JavaScript 它们本身也在不断地进化, 不断出现越来越多方便的语法糖, 但追根到底很多都是从函数式语言汲取的精华. 比如高阶函数, lambada 表达式, 柯里化等.
于是从探险的角度, 以好奇的心态开始学习函数式语言, 探索这个宝库, 拾取可供临摹的珍宝. 最起码它能让你多一个不同的角度看待编程语言, 影响你的思考方式. 学习的对象当然选择函数式语言的集大成者 - Haskell.
什么是 Haskell 和函数式编程
Haskell 是一门纯粹函数式的语言.
函数式编程是面向数学的抽象, 将计算描述为一种表达式求值. 命令式编程是关于解决问题的步骤, 函数式编程是关于数据的映射. 在纯粹函数式程式语言中, 你不是像命令式语言那样命令计算机要做什么, 而是通过用函数来描述出问题是什么, 也就是所谓范畴论中的映射关系. 函数式语言有以下的特性:
函数是一等公民, 可以在任何地方定义, 在函数内或函数外, 可以作为函数的参数和返回值, 可以对函数进行组合
变量的值是不可变的(immutable), 也就是说不允许像命令式编程语言中那样多次给一个变量赋值.
函数式语言的条件语句, 循环语句等也不是命令式编程语言中的控制语句, 而是函数的语法糖
抽象数据类型
灵活的多态
高阶函数(Higher-order function)
柯里化(Currying)
闭包(Closure)
函数式编程的优点
函数式的最主要的好处主要是不可变性带来的. 没有可变的状态, 函数就是引用透明 (Referential transparency) 的和没有副作用(No Side Effect).
函数即不依赖外部的状态也不修改外部的状态, 函数调用的结果不依赖调用的时间和位置, 这样写的代码容易进行推理, 不容易出错. 这使得单元测试和调试都更容易.
由于 (多个线程之间) 不共享状态, 不会造成资源争用 (Race condition), 也就不需要用锁来保护可变状态, 也就不会出现死锁, 这样可以更好地并发, 能够更好地利用多个处理器(核) 提供的并行处理能力.
Haskell 基本语法
变量和函数
一起介绍是因为在我看来, haskell 中变量和函数是没有区别的. 它们都是表达式, 根据表达式的不同形式, 分别对应到命令式语言中变量和函数的概念. 而且 haskell 中 变量 赋值后就是不可变的, 该 变量 就等于被赋予的值, 与命令式语言中 变量 是内存地址的引用是完全不同的概念. 硬要对应的话它更像是 C# 中的不可变量 const 或 static readonly .
你能从下面代码中区分出哪些是变量, 哪些是函数吗?
a = 1 -- 变量
arr = map (*2) [1,2,3] -- 变量还是函数?
maxNum = foldr max 0 -- 函数
-- 执行
- a
- > 1
- arr
- > [2,4,6]
- maxNum [3,5,1]
- > 5
定义函数: 函数名 参数 = 代码
调用函数: 函数名 参数
调用函数不用大括号( ), 注意的是函数首字母不能大写. 还有 maxNum 看不到形式参数是因为柯里化可以去掉参数, 后面会介绍.
if else
haskell 中 if else 表达式中的 else 部分不能省略, 也就是你不能只有 if 部分
-- 等于小于大于 0 分别对应 0,-1,1
- sign x = if x == 0 then 0
- else if x <0 then -1
- else 1
- case of
case of 表达式, 与其他语言的 switch case 类似.
-- 求出列表第一项
- head' xs = case xs of
- [] -> "No head for empty lists!"
- (x:_) -> show x
-- 执行
- head'"hello"
- >'h'
- head' [3,2,1]
- > 3
函数模式匹配
函数模式匹配的方式定义 head', 以及定义阶乘函数 factorial, 它本质上就是 case of 的语法糖. 函数模式匹配, 减少了一大堆类似 if else 的判断逻辑, 是我最喜欢的特性之一.
-- 求出列表第一项
head'[] ="No head for empty lists!"head' (x:_) = show x
-- 阶乘
- factorial 0 = 1
- factorial n = n * factorial (n - 1)
-- 执行
- head' [3,2,1]
- > 3
- factorial 5
- > 120
guards 和 where
guards, 类似 if else 表达式, 但可读性更强, where 语句定义的是局部变量表达式, 它只能放在语句尾部, guards 同样也是非常好的定义方式.
- bmiTell weight height
- | bmi <= 18.5 = "You're underweight,you emo,you!" | bmi <= 25.0 ="You're supposedly normal. Pffft,I bet you're ugly!" | bmi <= 30.0 ="You're fat! Lose some weight,fatty!"
- | otherwise = "You're a whale,congratulations!"
- where bmi = weight / height ^ 2
- let in
let in 表达式, let 中绑定的名字仅对 in 部分可见.
-- 圆柱体面积
- cylinder r h =
- let sideArea = 2 * pi * r * h
- topArea = pi * r ^2
- in sideArea + 2 * topArea
递归
我们使用递归来实现斐波那契数列和快速排序, haskell 写的快速排序是我见过的最容易理解的版本了, 专门为解决数学问题而生的 haskell 在解决算法和数据结构方面果然是不同凡响.
-- 斐波那契数列
- fab 1 = 1
- fab 2 = 1
- fab n = fab (n-1) + fab (n-2)
-- 快速排序
- quicksort [] = []
- quicksort (x:xs) =
- let smallerSorted = quicksort [a | a <- xs, a <= x]
- biggerSorted = quicksort [a | a <- xs, a> x]
- in smallerSorted ++ [x] ++ biggerSorted
尾递归实现常用的 map 和 filter 函数
[] 表示空列表
_ 匹配的是任意值.
(x:xs) 非常有用的列表匹配模式, x 表示第一项, xs 表示除去第一项之后的部分. 使用 (x:xs) 可以方便的实现尾递归
- -- map
- map' f [] = []
- map'f (x:xs) = f x : map' f xs
- -- filter
filter' _ []= [] -- _代表任意值
- filter' f (x:xs)
- | f x = x : filter' f xs
- | otherwise = filter' f xs
数据类型
了解了 haskell 基本语法后, 我们再进一步了解 haskell 基本数据类型
:type 获取任何表达式的类型, 可以用简写形式 :t
基本数据类型
Int 表示整数
Integer 也是整数, 但表示的是无界的, 所以可以表示非常大的数
Float 表示单精度的浮点数
Double 表示双精度的浮点数
Bool 表示布尔值, 它只有两种值: True 和 False
Char 表示一个字符. 一个字符由单引号括起, 一组字符的 List 即为字符串
List 列表中所有的项都必须是同一类型.
Tuple 的类型取决于它的长度及其项的类型.
- :t 1 -- Number
- 1 :: Num p => p
- :t 1::Integer
- 1::Integer :: Integer
- :t 1::Float
- 1::Float :: Float
- :t False -- Bool
- False :: Bool
:t 'c' -- 字符
'c' :: Char
:t "hello" -- 字符串
"hello" :: [Char]
:t [1,2,3] -- 列表 list
- [1,2,3] :: Num a => [a]
- :t [("hi",1),("there",2)] -- Tuple
- [("hi",1),("there",2)] :: Num b => [([Char], b)]
1::Integer 表示直接指定类型, 如果不指定编译器会自动推导出类型, 数字类型会推导出 Number 类型, 它包括 Int,Integer,Float,Double
[Char] 和 String 表示的都是字符串类型
[1,2,3] :: Num a => [a] 列表中的 a 表示任意类型, 意思你可以是 Bool,Stirng,Int 等等
[("hi",1),("there",2)] 这就是 Tuple 类型, 列表里面的每个项都用 () 包起来, 其中的每个项的元素数据类型必须相同, 每个 tuple 中元素个数必须相等, 但是每个 tuple 中的项可以不同类型, 比如 ("hi",1) 中一个是字符串, 一个是 Int.
函数也有类型, 定义函数的时候, 加上参数的类型和输出类型是好习惯.
&&,||,not 表示与或非逻辑
== 表示等于
/= 表示不等于
++ 连接列表, 相当于 concat
a, b 这种类型参数, 表示可以传入任何类型.
(Num a, Num p, Ord a) => a -> p 在 => 之前表示的是类型约束, 这里的 a 限定只能是 Num 类型和 Ord 类型. Num 表示数字类型, Ord 则表示可比较大小的型别, 包含如下三种型别之一: GT, LT, EQ.
:t head -- 取列表第一项的函数
head :: [a] -> a
:t sign -- sign 函数
sign :: (Num a, Num p, Ord a) => a -> p
:t (==) -- 是否相等
(==) :: Eq a => a -> a -> Bool
:t (++) -- 列表连接函数
(++) :: [a] -> [a] -> [a]
-- 执行
- sign 2
- > 1
- head [3,2,1]
- > 3
- "abc" == "bbc"
- > False
- "hello" ++ "world"
- > "hello world"
List 和 List comprehension
列表常用的函数
null 列表是否为空
length 返回列表长度
head 返回列表第一个元素
tail 返回列表除第一个元素以后的所有元素
last 返回列表最后一个元素
init 返回列表除最后一个元素之前的所有元素
take n 返回列表前 n 个元素
drop n 丢弃列表前 n 个元素
maximum 返回最大的元素
minimum 返回最小的元素
sum 返回元素的和
elem 元素是否包含于列表
list range
方便的 range, 尾递归加上 list range, 你真的还需要命令式语言中的循环语句吗?
[1..10] -- 1 到 10 的列表
> [1,2,3,4,5,6,7,8,9,10]
['a'..'z'] -- a 到 z 的字母字符串
> "abcdefghijklmnopqrstuvwxyz"
take 10 [1,3..] -- 前 10 个奇数
> [1,3,5,7,9,11,13,15,17,19]
take 10 (cycle[1,2,3]) -- 取前 10 的 [1,2,3] 序列
> [1,2,3,1,2,3,1,2,3,1]
take 5 $ repeat 3 -- 取前 5 项的 3 序列
> [3,3,3,3,3]
replicate 5 10 -- 相比 take repeat 更方便的用法
- > [10,10,10,10,10]
- list comprehension
list comprehension 相当于 map 和 filter 的函数的增强版, | 之前等于 map, | 之后等于 filter, 尤其在多限制条件和同时实现 map,filter 功能时更加明显. 是个非常强大和有用的特性, 完全可以替代列表的 map 和 filter 函数.
list comprehension 其实是由 monad 或 applicative functor 生成的语法糖.
[x*2 | x <- [1..10], x*2>= 12] -- 取乘以 2 后大于等于 12 的元素, 等于 map 结合 filter
> [12,14,16,18,20]
[if x `mod` 2 == 0 then "even" else "odd" | x <- [1..10]] -- 偶数转换为 even, 基数为 odd, 等于 map
> ["odd","even","odd","even","odd","even","odd","even","odd","even"]
[ x | x <- [10..20], x /= 13, x /= 15, x /= 19] -- 取除了 13,15,19 之外的元素, 多个限制条件, 等于 filter
> [10,11,12,14,16,17,18,20]
[ x*y | x <- [2,5,10], y <- [8,10,11]] -- 求两个列表所有可能的组合
> [16,20,22,40,50,55,80,100,110]
-- 嵌套的列表, 在不拆开它的前提下除去其中的所有奇数
- let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
- [ [ x | x <- xs, even x ] | xs <- xxs]
- > [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]
-- 取得所有三边长度皆为整数且小于等于 10, 周长为 24 的直角三角形
- [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
- > [(6,8,10)]
参考资料
HASKELL 趣学指南 http://wiki.jikexueyuan.com/project/haskell-guide/
Real World Haskell http://rwh.readthedocs.io/en/latest/index.html
来源: https://www.cnblogs.com/edwardloveyou/p/9439626.html