Haskell學習-函數式編程初探


原文地址:Haskell學習-函數式編程初探
為什么要學習函數式編程?為什么要學習Haskell?

.net到前端,C#和JavaScript對我來說如果談不上精通,最起碼也算是到了非常熟悉的程度。這兩門語言就像是我的盾牌和寶劍,給我保駕護航,開山劈石,伴隨着我不斷成長。同時C#和JavaScript它們本身也在不斷地進化,不斷出現越來越多方便的語法糖,但追根到底很多都是從函數式語言汲取的精華。比如高階函數,lambada表達式,柯里化等。

於是從探險的角度,以好奇的心態開始學習函數式語言,探索這個寶庫,拾取可供臨摹的珍寶。最起碼它能讓你多一個不同的角度看待編程語言,影響你的思考方式。 學習的對象當然選擇函數式語言的集大成者-Haskell。

什么是Haskell和函數式編程

Haskell 是一門純粹函數式的語言。

函數式編程是面向數學的抽象,將計算描述為一種表達式求值。命令式編程是關於解決問題的步驟,函數式編程是關於數據的映射。在純粹函數式程式語言中,你不是像命令式語言那樣命令計算機「要做什么」,而是通過用函數來描述出問題「是什么」,也就是所謂范疇論中的映射關系。函數式語言有以下的特性:

  • 函數是一等公民,可以在任何地方定義,在函數內或函數外,可以作為函數的參數和返回值,可以對函數進行組合
  • 變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個變量賦值。
  • 函數式語言的條件語句,循環語句等也不是命令式編程語言中的控制語句,而是函數的語法糖
  • 惰性求值
  • 抽象數據類型
  • 靈活的多態
  • 高階函數(Higher-order function)
  • 柯里化(Currying)
  • 閉包(Closure)

函數式編程的優點

函數式的最主要的好處主要是不可變性帶來的。沒有可變的狀態,函數就是引用透明(Referential transparency)的和沒有副作用(No Side Effect)。

  1. 函數即不依賴外部的狀態也不修改外部的狀態,函數調用的結果不依賴調用的時間和位置,這樣寫的代碼容易進行推理,不容易出錯。這使得單元測試和調試都更容易。
  2. 由於(多個線程之間)不共享狀態,不會造成資源爭用(Race condition),也就不需要用鎖來保護可變狀態,也就不會出現死鎖,這樣可以更好地並發,能夠更好地利用多個處理器(核)提供的並行處理能力。

Haskell基本語法

  1. 變量和函數
    一起介紹是因為在我看來,haskell中變量和函數是沒有區別的。它們都是表達式,根據表達式的不同形式,分別對應到命令式語言中變量和函數的概念。 而且 haskell變量 賦值后就是不可變的,該 變量 就等於被賦予的值,與命令式語言中 變量 是內存地址的引用是完全不同的概念。 硬要對應的話它更像是 C# 中的不可變量 conststatic 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看不到形式參數是因為柯里化可以去掉參數,后面會介紹。

  2. 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
    
  3. 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
    
  4. 函數模式匹配
    函數模式匹配的方式定義 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
    
  5. guardswhere
    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
    
  6. let in
    let in 表達式,let 中綁定的名字僅對 in 部分可見。

    -- 圓柱體面積
    cylinder r h =
        let sideArea = 2 * pi * r * h
            topArea = pi * r ^2
        in  sideArea + 2 * topArea
    

遞歸

  1. 我們使用遞歸來實現斐波那契數列和快速排序,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
    
  2. 尾遞歸實現常用的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基本數據類型

  1. :type 獲取任何表達式的類型,可以用簡寫形式 :t

  2. 基本數據類型

    • 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。
  3. 函數也有類型,定義函數的時候,加上參數的類型和輸出類型是好習慣。

    • &&、||、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

  1. 列表常用的函數
    null 列表是否為空
    length 返回列表長度
    head 返回列表第一個元素
    tail 返回列表除第一個元素以后的所有元素
    last 返回列表最后一個元素
    init 返回列表除最后一個元素之前的所有元素
    take n 返回列表前n個元素
    drop n 丟棄列表前n個元素
    maximum 返回最大的元素
    minimum 返回最小的元素
    sum 返回元素的和
    elem 元素是否包含於列表

  2. 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]
    
    
  3. list comprehension
    list comprehension 相當於map 和 filter的函數的增強版, | 之前等於map, | 之后等於filter, 尤其在多限制條件和同時實現map,filter功能時更加明顯。是個非常強大和有用的特性,完全可以替代列表的 map 和 filter 函數。
    list comprehension 其實是由 monadapplicative 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 趣學指南》
《Real World Haskell》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM