原文地址:Haskell學習-函數語言程式設計初探
為什麼要學習函數語言程式設計?為什麼要學習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)]