Haskell

Haskell（ˈhæskəl）是一種標準化的，通用的純函數式編程語言，有惰性求值和強靜態類型。它的命名源自美國邏輯學家哈斯凱爾·加里，他在數理邏輯方面上的工作使得函數式程式語言有了廣泛的基礎。在Haskell中，「函數是頭等物件」。作為一門函數程式語言，主要控制結構是函數。Haskell語言是1990年在程式語言Miranda語言的基礎上標準化的，並且以λ演算為基礎發展而來。這也是為什麼Haskell語言以希臘字母「λ」（Lambda）作為自己的標誌。Haskell具有「證明即程序、命題為類型」的特徵。

## 歷史

1985年，Miranda發行後，惰性函數式語言的關注度增長。到1987年前，出現了十多種非限定性、純函數式語言。其中，Miranda使用的最為廣泛，但還沒有出現在公共領域。在俄勒岡波特蘭的函數式程式語言與電腦結構大會（FPCA '87）上，參加者一致同意形成一個委員會來為這樣的語言定義一種開源標準。該委員會旨在整合已有函數式語言，作為將來的函數式語言設計研究工作的基礎。

### Haskell 1.0至1.4

能夠確使類型安全的運算符重載的類型類，是由Philip Wadler和Stephen Blott最初為Standard ML提議的，但首先在Haskell中於1987年至版本1.0期間實現。

1990年定義了Haskell的第一個版本（「Haskell 1.0」）。委員會形成了一系列的語言定義（1.0，1.1，1.2，1.3，1.4）。

<image>

### Haskell 98

1997年底，該系列形成了Haskell 98，旨在定義一個穩定、最小化、可移植的語言版本以及相應的標準庫，以用於教學和作為將來擴展的基礎。委員會明確歡迎創建各種增加或集成實驗性特性的Haskell 98的擴展和變種。

1999年2月，Haskell 98語言標準公布，名為《The Haskell 98 Report》。2003年1月，《Haskell 98 Language and Libraries: The Revised Report》公布。接著，格拉斯哥Haskell編譯器實現了當時的事實標準，Haskell快速發展。

### Haskell Prime

2006年早期，開始了定義Haskell 98標準後續的進程，非正式命名為Haskell Prime。這是個修訂語言定義的不斷增補的過程，每年產生一個新的修訂版。第一個修訂版於2009年11月完成、2010年7月發布，稱作Haskell 2010。

### Haskell 2010

Haskell 2010加入了外部函數接口（FFI）允許綁定到其它程式語言，修正了一些語法問題（在正式語法中的改動）並廢除了稱為「n加k模式」（換言之，不再允許形如`fact (n+1) = (n+1) * fact n`的定義）。引入了語言級編譯選項語法擴展（Language-Pragma-Syntax-Extension），使得在Haskell原始碼中可以明確要求一些擴展功能。Haskell 2010引入的這些擴展的名字是DoAndIfThenElse、HierarchicalModules、EmptyDataDeclarations、FixityResolution、ForeignFunctionInterface、LineCommentSyntax、PatternGuards、RelaxedDependencyAnalysis、LanguagePragma、NoNPlusKPatterns。

## 特性

Haskell是現有的一門開放的、已發布標準的，且有多種實現的語言。支持惰性求值、模式匹配、列表解析、類型類和類型多態。它是一門純函數式程式語言，這意味著大體上，Haskell中的函數沒有副作用。Haskell用特定的類型來表達副作用，該類型與函數類型相互獨立。純函數可以操作並返回可執行的副作用的類型，但不能夠執行它們，只有用於表達副作用的類型才能執行這些副作用，Haskell以此表達其它語言中的非純函數。

Haskell擁有一個基於Hindley-Milner類型推論的靜態、強類型系統。Haskell在此領域的主要創新就是加入了類型類，原本設想作為重載的主要方式，在之後發現了更多用途。

Haskell的主要實現GHC是個解釋器，也是個原生代碼編譯器。它可以在大多數平台運行，GHC在並發和並行上具有高性能的實現能力，也有豐富的類型系統，如廣義代數數據類型和類型家族）。

單子是一個抽象類型，可以表達不同種類的計算，包括異常處理、非確定性、語法分析以及軟體事務內存，其中一個應用是用於表達副作用的類型。單子定義為普通的數據類型，同時Haskell也為其提供了幾種語法糖。

Haskell有一個活躍的社區，在線上包倉庫Hackage上有豐富的第三方開源庫或工具。

## 語法

### 數據類型

Haskell是強類型語言。Char的字面值用單引號圍起；字串即Char類型，其字面值用雙引號括起來。Int通常為32位整型；Integer是無界整型；Float 表示單精度的浮點數；Double 表示雙精度的浮點數；Bool 只有兩種值：True 和 False。

#### List

list使用中括號`[ ]`與逗號分隔符`,`定義，如`[1,2,3]`。list元素必須具有相同類型。字串是list的特例。list的基本運算符為`:`，用於表示把一個元素與一個list（即其他元素）連接（cons）起來。`:`是右結合的運算符。`[1,2,3]`實際上是 `1:2:3:[]` 的語法糖。兩個 List 合併通過 `++` 運算符實現。按照索引取得 List 中的元素，可以使用 `

` 運算符。List 中的 List 可以是不同長度，但必須得是相同的型別。'K'..'Z'這樣的Range方法快捷定義一個List。2,4..20用法給出了Range的第一、第二、最後一個元素。使用 > 和 >= 可以比較 List 的大小。它會先比較第一個元素，若它們的值相等，則比較下一個，以此類推。List常用的函數:

- head 返回一個 List 的頭部，也就是 List 的首個元素。
- tail 返回一個 List 的尾部，也就是 List 除去頭部之後的部分。
- last 返回一個 List 的最後一個元素。
- init 返回一個 List 除去最後一個元素的部分。
- length 返回一個 List 的長度。
- null 檢查一個 List 是否為空。如果是，則返回 True，否則返回 False。
- reverse 將一個 List 反轉
- take 返回一個 List 的前幾個元素。例如take 24 13,26..取前24個13的倍數
- drop 刪除一個 List 中的前幾個元素
- maximum 返回一個 List 中最大的元素。
- minimun 返回最小的元素。
- sum 返回一個 List 中所有元素的和。
- product 返回一個 List 中所有元素的積。
- elem 判斷一個元素是否在包含於一個 List，通常以中綴函數的形式調用
- replicate 得到包含相同元素的 List。例如：replicate 3 10，得 10,10,10。
- repeat 產生一個元素的無限重複的List
- cycle 接受一個 List 做參數並返回一個無限 List

list comprehension是指基於一個List，按照規則產生一個新List，例如：x*2

#### Tuple

使用與逗號分隔符，定義一個tuple的實例。其元素可為不同類型，但個數是固定的。

Tuple的類型取決於其中項的數目與其各自元素的類型。單元素的 Tuple 被禁止。

- fst 返回一個序對的首項。
- snd 返回序對的尾項
- zip 取兩個 List作為參數，然後將它們依次配對，形成一組序對的 List。

### 運算符

基本類似於C語言。但使用not表示邏輯非。

```
 +  -  *  /  ^    -- 加、减、乘、除、幂
 mod              -- 取余数
 $                -- 也是表示函数作用的, 但它的优先级最低, 而且作用顺序是从右向左的
 ++               -- 两个List的合并
 .                 -- 函数的复合
 && ||  ==  /=     -- 与、或、等于、不等于
 <=  >=  <  >      -- 小于等于、大于等于、小于、大于
 : //  =  @        -- 一個元素連接List。
 ->               -- 函數類型描述，運算符左邊為參數類型，右邊為結果類型。為右结合。例如：addThree :: Int -> Int -> Int -> Int 
 =>               -- 運算符的左邊表示一個類型變量（通常為單個小寫字母）屬於一個類型類（Typeclass），相當於C++語言的模板參數類型
 ..               -- List的Range限定
 ::               -- 函數/表達式的類型特徵，左側為函數/表達式，右側為類型
 <-             -- List comprehension 的條件限定
 

             -- 取List的第某個元素，下標從0開始
```

基本的 Typeclass：

- Eq 可判斷相等性的type
- Ord 可比較大小的type
- Show 可表示為字串的type
- Read 可從字串轉換出值的type
- Enum 連續的，也就是可枚舉的type。每個值都有後繼 (successer) 和前置 (predecesor)，分別可以通過 succ 函數和 pred 函數得到。
- Bounded 有上限和下限。例如：maxBound :: Char 或者 maxBound :: Bool
- Num 數字
- Integral
- Floating

### 表達式

- let表達式：格式為 let bindings in expressions。let 也可以定義局部函數。在一行中設定多個名字的值，可用分號隔開。List Comprehension 中 let綁定的名字在輸出函數及限制條件中都可見；忽略了 let 綁定的 in 部分，因為名字的可見性已經預先定義好了。
- if then else是表達式

```
 case expression of pattern -> result  
      pattern -> result  
      pattern -> result  
      ...  
```

### 控制結構

if then else是分段函數定義時的語法糖。與C語言不同，要求必須有else部分。類似於C語言分支語句的情形，叫做pattern matching，例子如下：

```Haskell
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts x
    | x <= 6    = 7 - x
    | otherwise = 0

(||) :: Bool -> Bool -> Bool  -- 或操作的类型与定义
True  || _ = True             -- 第二个参数是任何值都匹配。 
False || y = y
```

### 函數

- 函數調用有最高運算順序，例如`succ 9*10`表示`(succ 9)*10`。
- 函數的調用使用空格符而不是括號。
- 函數的複合調用是左結合。
- 首字母大寫的函數是不允許的。
- 兩個參數的函數的調用可以寫成中綴形式：param1 `funcName` param2。
- 運算符可以用括號圍起來，作為前綴形式：`(+) 2 3` 的結果為5。
- 在 ghci 下，我們可以使用 `let` 關鍵字來定義一個常量。在 ghci 下執行 `let a=1` 與在腳本中編寫 `a=1` 是等價的。

```Haskell
--funcName arguments = expression  --定义函数的一般形式
area r = pi * r ^ 2 -- 定义了一个函数
area 101 -- 调用了函数
f1 f2 3.14 -- 函数调用是左结合，等效于(f1 f2) 3.14

--模式匹配方式定义
factorial :: (Integral a) => a -> a  
factorial 0 = 1  
factorial n = n * factorial (n - 1)   

--as模式，是将一个名字和 @ 置于模式前，可以在按模式分割参数值时仍保留对其整体的引用。如nameGlobal@(x:y:ys)，nameGlobal会匹配出与 x:y:ys 对应的东西。as模式在较大的模式中保留对整体的引用，从而减少重复性的工作。

heron a b c = sqrt (s * (s - a) * (s - b) * (s - c))
    where                     -- where在表达式中局部绑定了名字s与一个值。也可以在表达式之前用let ... in语法
    s = (a + b + c) / 2

absolute x                 -- 绝对值函数，使用了分段函数语法糖（称作Guards）
    | x < 0     = 0 - x
    | otherwise = x        -- 兜底条件

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | bmi <= 18.5 = "You're underweight."  
    | bmi <= 25.0 = "You're normal. "  
    | bmi <= 30.0 = "You're fat."  
    | otherwise   = "You're overweight."  
    where bmi = weight / height ^ 2  -- 使用where定义多个名字来避免重复

--where也可以用模式匹配
initials :: String -> String -> String  
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."  
    where (f:_) = firstname  
          (l:_) = lastname  

--where可以定义函数。在定义一个函数的时候也写几个辅助函数摆在 where 绑定中。 而每个辅助函数也可以透过 where 拥有各自的辅助函数。
calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi w h | (w, h) <- xs] 
    where bmi weight height = weight / height ^ 2  

funcName :: type1 -> type2 -> type3 -- 其中，::表示类型特征(type signature)，->是右结合，这里等效于type1 -> (type2->type3)，给定一个type1的输入参数，返回一个函数(type2->type3)

f1 = (absolute .  area)   -- 函数复合运算符是 . (function composition operator)
```

多態類型(Polymorphic types)類似於C++的模板。例如，算術加法：

```Haskell
 (+) :: (Num a) => a -> a -> a -- Num是typeclass。 =>表示signature restricts 
```

### lambda函數

lambda 就是匿名函數。寫法是：一個 `\`（因為它看起來像是希臘字母λ），後面是用空格分隔的參數，`->` 後面是函數體。通常用括號將括起lambda函數，否則它會占據整個右邊部分。

例如：`(\a b -> (a * 30 + 3) / b)`

可以在 lambda 中使用模式匹配，但無法為一個參數設置多個模式，如 `[]` 和 `(x:xs)` 並用。

使用 lambda 可以更明確地表現出值是個函數，可以用來傳遞給其他函數作參數。

### 高階函數

Haskell的所有函數實際上是單參數函數。多參數函數的寫法實際上是Curry化的語法糖。即 `func a b` 等價於 `(func a) b`。

point free style (也稱作 pointless style) 的函數，即通過柯里化 (Currying)省略掉單參數。例如：

```Haskell
 sum' :: (Num a) => [a] -> a     
 sum' xs = foldl (+) 0 xs   --等号的两端都有个 xs。 
 sum'   = foldl (+) 0     --柯里化 (Currying)，可以省掉两端的 xs。
```

中綴運算符可以加括號變為單參數函數。如 `(*3) 5` 的值為15。但 `(-5)` 表示負值，所以單參數函數需要寫為 `(subtract 5)`。

中綴運算符 `$` 可用於改變函數的調用次序，使其右邊的表達式先計算出來。這可以減少一對括號使用。例如 `f (g (z x))` 與 `f $ g $ z x` 等價。其定義是：

```Haskell
 ($) :: (a -> b) -> a -> b  
 f $ x = f x  
```

`$` 還可以將數據作為函數使用。例如：

```Haskell
 map ($ 3) [(4+),(10*),(^2),sqrt]
```

中綴運算符 `.` 用於函數的複合，其定義是：

```Haskell
 (.) :: (b -> c) -> (a -> b) -> a -> c  
 f . g = \x -> f (g x)
```

### 異常處理

提供了處理異常的函數、catch、finally/.

```haskell
import Prelude hiding(catch)
import Control.Exception
instance Exception Int
instance Exception Double
main = do
  catch
    (catch
      (throw (42::Int))
      (\e-> print (0,e::Double)))
    (\e-> print (1,e::Int))
```

輸出結果

```
(1,42)
```

類似於 C++

```cpp
#include <iostream>
using namespace std;
int main() {
    try {
        throw (int)42;
    } catch (double e) {
        cout << "(0," << e << ")" << endl;
    } catch (int e) {
        cout << "(1," << e << ")" << endl;
    }
}
```

另外一個例子：

```haskell
do {
  -- Statements in which errors might be thrown
} `catch` \ex -> do {
  -- Statements that execute in the event of an exception, with 'ex' bound to the exception
}
```

如果僅有一個錯誤條件，Maybe 類足夠用了，確省是Haskell的 Monad . 更複雜的出錯處理用Error 或ErrorT monads, 類似功能可用`catch`。

## 示例

### Haskell的函數結構

如下是Haskell語言的"Hello world"，注意其中除最後一行外皆可省略。

```Haskell
module Main where

main :: IO ()
main = putStrLn "Hello, World!"
```

如下是階乘函數的Haskell實現：

```Haskell
fac :: Int -> Int
fac 0 = 1
fac n = n * fac (n - 1)
```

它將階乘描述成有一個基本終止情形的遞歸函數。這跟數學定義中對階乘的描述很相似。事實上，Haskell中很多的代碼的語法與功能都和數學一致。

上面的遞歸函數的第一行是可選的，它描述了這個函數的型態（types）。它可以讀作函數fac的型態為整數至整數（function fac has a int-to-int type）。這就是說，它以一個整型為參數，並且返回另一個整型。

第二行依賴的模式匹配，是Haskell程序中一個重要的部分。注意函數的參數是用空格分隔而不是在括號中。當函數的參數是0時，它會返回整型1。對於其他的情況則嘗試第三行。這是一個遞歸，它會一直執行只到滿足基本的情形。負參數會導致無限遞歸，一個guard保證第三行不會執行負參數。

"Prelude"是一個類似C中標準庫的小函數集合。使用Prelude，並用無指定參數的寫法，它可以改成：

```Haskell
fac = product . enumFromTo 1
```

上面的定義接近於數學中的定義：f = g o h（參見複合函數），這並不是一個對變量賦值的語句。

### Haskell的高階函數

Haskell中可以定義高階函數（Higher-order Function），既將函數作為一個參數來使用，也可以將函數作為結果輸出，例如：

```Haskell
f :: (Int -> Int) ->(Int -> Int)
f g = \x -> g x + 5
```

這裡f就是一個高階函數，它取一個從Int到Int的函數g作為參數，輸出一個從Int到Int的函數。高階函數的使用在一些情況下將極大的簡化代碼。

## Haskell的編譯器

- Hugs
- ghc (Glasgow Haskell Compiler)
- yhc (York Haskell Compiler)
- nhc98

## 外部連結

- Haskell的官方主頁
- HaskellWiki：Haskell社群主網站
- UNSW的Haskell教程
- Haskell笑話
- Haskell舊的討論主題
- Haskell vs. Ada vs. C++ vs. Awk vs. ......一個實驗性的前衛語言（PostScript檔案）
- Haskell編程的變革 -- 一個輕快的Haskell編程風格總纜
- 一個Haskell研究者的線上介紹
- ePolyglot -- 將Haskell、Python與Eiffel結合的語言
- Haskell函式庫

### 教學手冊

- 用Haskell寫程式，Graham Hutton撰寫
- 另一個Haskell教學手冊 -- 由Hal Daume III所寫的好手冊，需要比官方教學手冊少的先備知識
- A Gentle Introduction to Haskell 98（較高階的教學手冊，有 PDF檔案）
- Haskell Tutorial for C Programmers，Eric Etheridge撰寫
- 在Haskell.org的教學手冊列表
- Haskell編程典範
- Haskell趣學指南
- Haskell趣學指南

## 基本資料

- 說明：Haskell的徽標
- 編程范型：純函數式
- 發行日期：1990年
- 設計者：Lennart Augustsson, Dave Barton, Brian Boutel, Warren Burton, Joseph Fasel, Kevin Hammond, Ralf Hinze, 保羅·胡達客, John Hughes, Thomas Johnsson, Mark Jones, 西蒙·佩頓·瓊斯, John Launchbury, Erik Meijer, John Peterson, Alastair Reid, Colin Runciman, 菲利普·瓦德勒
- 類型系統：推論, 靜態, 強類型
- 軟體實現：GHC, , NHC, JHC, Yhc
- 受影響於：Clean, FP, Gofer (程式語言), Hope和Hope+, Id (程式語言), ISWIM, KRC, Lisp, Miranda, ML和Standard ML, Orwell (程式語言), SASL, Scheme, SISAL
- 施影響於：Agda, Bluespec, C++11/Concepts, C#/LINQ, Cayenne, Clean, Clojure, CoffeeScript, Curry, Elm, Escher (程式語言), F#, Frege, Hack, Idris, Isabelle, Java/Generics, LiveScript, Mercury, PureScript, Python, Raku, Rust, Scala, Swift, Timber, Visual Basic 9.0
- 作業系統：跨平臺
- 文件擴展名：`.hs`, `.lhs`