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`