Skip to content

Haskell 速查表

静态类型的纯函数式编程语言。

01

入门

基础与 GHCi

Haskell 是纯函数式语言。GHCi 是交互式 REPL。函数应用不需要括号——succ 5 等同于其他语言中的 succ(5)。字符串只是字符的列表。

haskell
-- GHCi interactive environment
-- launch: ghci  (quit with :q)

-- simple arithmetic
> 2 + 3
5

-- function application (no parentheses needed)
> succ 5
6

-- define a function
> double x = x * 2
> double 10
20

-- lists
> [1,2,3,4]
[1,2,3,4]

-- strings are lists of chars
> "hello"
"hello"

注释

Haskell 用 -- 表示单行注释,用 {- -} 表示多行注释。与多数语言不同,Haskell 的注释可以嵌套,注释掉已含注释的代码时很方便。

haskell
-- single line comment

{- multi-line comment
   spanning multiple lines -}

{- nested {- comments -} are allowed -}

-- comments are ignored by the compiler

Hello World

main 是每个 Haskell 程序的入口。putStrLn 输出字符串并换行。用 ghc 编译为原生可执行文件,或用 runhaskell 解释运行。

haskell
-- hello.hs
main :: IO ()
main = putStrLn "Hello, World!"

-- run interpreted
--   runhaskell hello.hs
-- or compile to an executable
--   ghc hello.hs && ./hello

GHCi 中的 let

在 GHCi 中,let 定义绑定不需要 in。在源文件中,顶层定义不使用 let,而表达式内的 let 需要配对的 in。这一差异常让初学者困惑。

haskell
-- in GHCi, 'let' defines a binding (no 'in' needed)
> let x = 5
> let double n = n * 2
> x + double x
15

-- in a source file, top-level bindings need no 'let'
y = 10

-- 'let' inside an expression needs 'in'
z = let a = 2 in a * a   -- 4

类型查看

:t(或 :type)显示表达式的类型;:: 读作“具有……类型”。:i(info)显示类型类实例与定义。这些 GHCi 命令是探索陌生代码的利器。

haskell
-- :t shows the type of an expression
> :t 'a'
'a' :: Char

> :t True
True :: Bool

> :t "hi"
"hi" :: [Char]

> :t (+)
(+) :: Num a => a -> a -> a

-- :info shows typeclass info
> :info Num

加载文件

:load 把模块加载进 GHCi;:reload 在编辑后重新加载。:browse 列出模块的所有导出。:main 用命令行参数运行程序。这些让 REPL 工作流很顺滑。

haskell
-- GHCi commands
> :load MyModule.hs    -- or :l
> :reload              -- or :r, reload after edits
> :type main           -- check a type
> :browse Data.List    -- list a module's exports
> :main arg1 arg2      -- run main with args

-- :set +t auto-prints types of results
02

类型

基本类型

Int 是定长整数;Integer 无界,可按需增长。为精度优先用 Double 而非 Float。Char 存放单个 Unicode 字符。用类型标注让类型更明确。

haskell
-- Int: bounded integer (platform-dependent)
x :: Int
x = 9

-- Integer: arbitrary-precision (unbounded)
big :: Integer
big = 2 ^ 100

-- Float / Double (prefer Double)
piVal :: Double
piVal = 3.14159

-- Bool
flag :: Bool
flag = True

-- Char (single Unicode character)
c :: Char
c = 'a'

类型标注

:: 为表达式或绑定添加类型标注。类型签名通常位于定义上方。它们提升可读性并让编译器更早发现错误。

haskell
-- explicit type signature above a binding
age :: Int
age = 30

-- annotate an expression inline with ::
result = (5 :: Int) + 3

-- function signature
add :: Int -> Int -> Int
add x y = x + y

-- annotate a list
nums :: [Double]
nums = [1.0, 2.0, 3.0]

类型变量

类型签名中的小写标识符是类型变量——可代表任意类型,使函数成为多态的。同一个变量在一条签名中必须指代同一类型。

haskell
-- 'a' is a type variable (polymorphic)
id' :: a -> a
id' x = x

-- works for any type
> id' 5
5
> id' "hello"
"hello"

-- length works on any list
length :: [a] -> Int

函数类型

-> 是右结合的:a -> b -> c 表示 a -> (b -> c)。最后一个类型是返回类型,其余是参数类型。函数可以接受或返回其他函数。

haskell
-- '->' is right-associative
add :: Int -> Int -> Int
-- equivalent to: Int -> (Int -> Int)

-- takes a function as an argument
apply :: (a -> b) -> a -> b
apply f x = f x

-- returns a function
const' :: a -> b -> a
const' x y = x

Maybe 与 Either

Maybe 表示可能失败的计算(Just 值或 Nothing)。Either 携带错误信息(Left 错误或 Right 值)。两者都替代 null 与异常,实现更安全的错误处理。

haskell
-- Maybe: optional values
data Maybe a = Nothing | Just a

safeDiv :: Double -> Double -> Maybe Double
safeDiv _ 0 = Nothing
safeDiv x y = Just (x / y)

-- Either: a value or an error
data Either a b = Left a | Right b

-- convention: Left = error, Right = success
parse :: String -> Either String Int
parse s = Right (read s)

元组与列表类型

元组 (a, b) 存放固定数量、可能不同类型的值。列表 [a] 存放任意数量、同一类型的值。元组的类型编码了其大小;列表的类型则不。

haskell
-- tuple: (a, b)
pair :: (Int, String)
pair = (1, "one")

-- list: [a]
nums :: [Int]
nums = [1, 2, 3]

-- list of tuples
pairs :: [(Int, String)]
pairs = [(1, "a"), (2, "b")]

-- empty list keeps the polymorphic type
empty :: [a]
empty = []
03

函数

函数定义

函数由名称、参数和 = 定义。应用只需一个空格——无需括号或逗号。add 3 4 实际上是 ((add 3) 4),这是柯里化的结果。

haskell
-- name, parameters, '=', body
double :: Int -> Int
double x = x * 2

-- application: just a space, no parentheses
> double 5
10

-- multiple parameters (curried)
add :: Int -> Int -> Int
add x y = x + y

> add 3 4
7

函数应用与 $

应用比运算符结合更紧,且是左结合的。$ 是低优先级、右结合的应用运算符,用于省略最右侧参数周围的括号。

haskell
-- application is left-associative
> max 3 5
5
> max (max 3 5) 7
7

-- '$' has the lowest precedence, right-associative
> sum (map (*2) [1..5])
30
> sum $ map (*2) [1..5]   -- same, fewer parens

-- chain reads left to right
> putStrLn $ show $ sum [1..10]
55

运算符作为函数

用括号包裹运算符即可作为函数使用:(+) 2 3。节(section)部分应用运算符:(+10)、(3/)。反引号把二元函数变为中缀:10 `div` 3 更自然。

haskell
-- wrap an operator in () to use as a function
> (+) 2 3
5
> (*) 4 5
20

-- sections: partial application of operators
> (+10) 5
15
> (3/) 12
0.25
> (/3) 12
4.0

-- backticks make a function infix
> 10 `div` 3
3
> 10 `mod` 3
1

函数组合

. 从右向左组合函数:(f . g) x = f (g x)。它支持无参风格,省略参数。链式调用从内向外读,但避免了层层括号。

haskell
-- (.) composes functions right to left
(.) :: (b -> c) -> (a -> b) -> a -> c
(f . g) x = f (g x)

> negate . sum $ [1,2,3]
-6

-- chaining
fn = ceiling . negate . tan
> fn (pi/4)
-1

-- point-free style
odd' = not . even

函数作为值

函数是一等公民:可作为参数传递、被返回、存入数据结构。applyTwice 接受一个函数并应用两次。这是高阶编程的基础。

haskell
-- functions are first-class values
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)

> applyTwice (+3) 10
16
> applyTwice (\x -> x*x) 2
16

-- pass functions to higher-order functions
> map (*2) [1..5]
[2,4,6,8,10]
> filter even [1..6]
[2,4,6]

中缀与前缀

任何二元函数都可用反引号写成中缀(3 `elem` xs),任何运算符都可用括号写成前缀(mod 10 3)。按可读性选择即可。

haskell
-- infix operators can be written prefix with ()
> mod 10 3
1
> div 10 3
3

-- functions can be written infix with backticks
> 10 `mod` 3
1
> 10 `div` 3
3

-- elem: membership test, often infix
> 3 `elem` [1,2,3]
True
04

列表

列表基础

列表是同类型元素的单链表。字符串就是 [Char]。:(cons)以 O(1) 在头部插入。列表不可变——操作返回新列表。

haskell
-- lists are homogeneous linked lists
nums :: [Int]
nums = [1, 2, 3, 4, 5]

-- strings are [Char]
chars = ['h','e','l','l','o']
str   = "hello"   -- same as chars

-- empty list
empty = []

-- cons (prepend) is O(1)
> 0 : [1,2,3]
[0,1,2,3]

范围列表

范围 [a..b] 生成列表。给出前两个元素即可设定步长:[2,4..10]。得益于惰性,无限列表如 [1..] 是合法的——用 take 取出有限前缀。

haskell
-- ranges
> [1..5]
[1,2,3,4,5]

-- with a step (first two elements)
> [2,4..10]
[2,4,6,8,10]

-- decreasing needs the step
> [5,4..1]
[5,4,3,2,1]

-- infinite lists (lazy)
> take 5 [1..]
[1,2,3,4,5]

> take 3 (cycle [1,2])
[1,2,1]

列表操作

head/tail/last/init 访问列表两端——小心,空列表上会崩溃(更推荐模式匹配)。length、null、reverse 是完全函数。由于列表是链表,多数访问是 O(n)。

haskell
-- basic access (partial: crash on empty)
> head [1,2,3]
1
> tail [1,2,3]
[2,3]
> last [1,2,3]
3
> init [1,2,3]
[1,2]

-- safe helpers
> length [1,2,3]
3
> null []
True
> reverse [1,2,3]
[3,2,1]

列表拼接

++ 拼接列表(左侧为 O(n))。: 以 O(1) 在头部加一个元素。concat 展开一层嵌套。intercalate 用分隔符连接列表。递归构建列表时优先用 :。

haskell
-- '++' concatenates two lists
> [1,2] ++ [3,4]
[1,2,3,4]

-- ':' prepends a single element (O(1))
> 1 : [2,3]
[1,2,3]

-- concat flattens one level
> concat [[1,2],[3,4],[5]]
[1,2,3,4,5]

-- intercalate joins with a separator
> intercalate ", " ["a","b","c"]
"a, b, c"

列表推导式

列表推导式类似集合构造式:[输出 | 生成器, 谓词, ...]。多个生成器产生笛卡尔积。谓词过滤,let 定义辅助值。

haskell
-- like mathematical set-builder notation
> [x*2 | x <- [1..5]]
[2,4,6,8,10]

-- with a predicate (filter)
> [x | x <- [1..10], even x]
[2,4,6,8,10]

-- multiple generators = Cartesian product
> [(x,y) | x <- [1,2], y <- ['a','b']]
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

-- with a let binding
> [x*y | x <- [1..3], let y = x+1]
[2,6,12]

常用列表函数

elem 检查成员关系(常写成中缀)。sum 与 product 对数值列表归约。take/drop 取或去掉前缀。splitAt 按索引切分成一对。因列表不可变,都返回新列表。

haskell
-- membership (often written infix)
> elem 3 [1,2,3]
True
> 3 `elem` [1,2,3]
True

-- reductions
> sum [1..10]
55
> product [1..5]
120

-- take / drop a prefix
> take 3 [1..10]
[1,2,3]
> drop 3 [1..10]
[4,5,6,7,8,9,10]

-- split at an index
> splitAt 3 [1..6]
([1,2,3],[4,5,6])
05

元组

元组基础

元组将固定数量、可能不同类型的值组合在一起。分量数是类型的一部分——(Int,Int) 与 (Int,Int,Int) 是不同类型。元组不可变。

haskell
-- tuples can hold different types
pair :: (Int, String)
pair = (1, "hello")

triple :: (Int, String, Double)
triple = (1, "x", 3.14)

-- the size is part of the type
-- (1,2) and (1,2,3) are different types

二元组函数

fst 与 snd 仅对二元组(pair)有效。swap 交换两个分量。其他长度的元组请用模式匹配。类型系统会阻止对三元组使用 fst。

haskell
-- fst/snd only work on pairs
> fst (1, "a")
1
> snd (1, "a")
"a"

-- swap exchanges the components
> swap (1, 2)
(2,1)

-- these do NOT work on triples
> fst (1, 2, 3)   -- type error!

元组模式匹配

模式匹配按位置解构元组。用 _ 忽略不需要的分量。元组模式可用于函数定义、let 绑定、列表推导式和 case 表达式。

haskell
-- destructure a tuple by position
describe :: (Int, String) -> String
describe (n, s) = show n ++ ": " ++ s

-- ignore components with _
firstOf :: (a, b, c) -> a
firstOf (x, _, _) = x

-- in a list comprehension
> [n | (n, _) <- [(1,'a'),(2,'b')]]
[1,2]

zip 与 unzip

zip 把两个列表的元素配对,到较短者为止。unzip 是逆操作,把 pair 列表拆成一对列表。zipWith 用函数合并配对元素,而非组成元组。

haskell
-- zip pairs elements from two lists
> zip [1,2,3] ['a','b','c']
[(1,'a'),(2,'b'),(3,'c')]

-- stops at the shorter list
> zip [1,2,3] [4,5]
[(1,4),(2,5)]

-- unzip splits a list of pairs
> unzip [(1,'a'),(2,'b')]
([1,2],"ab")

元组与列表

用列表存放同类型的变长序列,用元组存放定长的异构组合。列表长度不在类型中;元组元数在类型中。元组列表常用于表格数据。

haskell
-- list: same type, variable length
ints :: [Int]
ints = [1, 2, 3]

-- tuple: mixed types, fixed length
mixed :: (Int, String, Bool)
mixed = (1, "x", True)

-- list of tuples (tabular data)
people :: [(String, Int)]
people = [("Alice",30),("Bob",25)]

-- cannot mix types in a list
> [1, "two"]   -- type error
06

模式匹配

基本模式匹配

模式匹配自上而下尝试,第一个匹配者胜出。字面量模式匹配精确值。_ 是匹配任意值的通配符。务必加一个兜底分支,避免非穷尽匹配错误。

haskell
-- match on literals, top to bottom
lucky :: Int -> String
lucky 7 = "LUCKY SEVEN!"
lucky _ = "Sorry, you're out of luck"

> lucky 7
"LUCKY SEVEN!"
> lucky 13
"Sorry, you're out of luck"

列表模式

[] 匹配空列表;(x:xs) 把 x 绑定到头、xs 绑定到尾。(x:y:_) 绑定前两个元素。这些模式是递归处理列表的基础。

haskell
-- empty list
isEmpty [] = True
isEmpty _  = False

-- head and tail (cons pattern)
head' :: [a] -> a
head' (x:_) = x

-- first two elements
firstTwo :: [a] -> [a]
firstTwo (x:y:_) = [x,y]
firstTwo xs      = xs

元组模式

元组模式按位置绑定分量。用 _ 忽略不需要的分量。嵌套元组需要嵌套模式。这是解包元组值的惯用法。

haskell
-- match by position
addPair :: (Int, Int) -> Int
addPair (x, y) = x + y

-- nested patterns
getAge :: (String, (Int, Int)) -> Int
getAge (_, (age, _)) = age

-- ignore with _
nameOf :: (String, Int) -> String
nameOf (name, _) = name

As 模式

As 模式(name@pattern)在解构的同时把整个匹配值绑定到一个名字。当你同时需要原值与其组成部分时很有用,避免昂贵的重建。

haskell
-- '@' keeps the whole value AND its parts
firstLetter :: String -> String
firstLetter ""        = "Empty string!"
firstLetter all@(x:_) =
  "The first letter of " ++ all ++ " is " ++ [x]

> firstLetter "Haskell"
"The first letter of Haskell is H"

case 表达式

case 在表达式内进行模式匹配。当函数体需要在某些设置之后对某值分支时很有用。模式按顺序尝试;需要时可加兜底。

haskell
-- pattern match inside any expression
describe :: [a] -> String
describe xs =
  case xs of
    []  -> "empty"
    [_] -> "singleton"
    _   -> "longer"

-- case on any value, e.g. an Ordering
classify n = case compare n 0 of
  LT -> "negative"
  EQ -> "zero"
  GT -> "positive"

构造器模式

模式可以匹配数据构造器,如 Red、Nothing、Just。这是代数数据类型的解构方式。编译器会对非穷尽模式发出警告,所以请覆盖所有构造器。

haskell
-- match on data constructors
data Color = Red | Green | Blue

toString :: Color -> String
toString Red   = "red"
toString Green = "green"
toString Blue  = "blue"

-- match on Maybe
fromMaybe :: a -> Maybe a -> a
fromMaybe d Nothing  = d
fromMaybe _ (Just x) = x
07

守卫

基本守卫

守卫按顺序测试布尔条件;第一个为 True 的胜出。otherwise 定义为 True,作为兜底。对于多路分支,守卫比 if-then-else 更清晰。

haskell
-- '|' tests boolean conditions in order
bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "underweight"
  | bmi <= 25.0 = "normal"
  | bmi <= 30.0 = "overweight"
  | otherwise   = "obese"

> bmiTell 22.0
"normal"

otherwise 与兜底

otherwise 是 True 的别名,用作最后一个守卫以保证完备。没有兜底时,未匹配的输入会触发运行时错误。除非已覆盖全部情况,否则总以 otherwise 结尾。

haskell
-- otherwise is just True
otherwise :: Bool
otherwise = True

-- a signum implementation
signum' :: Int -> Int
signum' n
  | n < 0     = -1
  | n == 0    = 0
  | otherwise = 1

带 where 的守卫

守卫可以使用多个参数。where 子句让所有守卫共享辅助定义——这里 bmi 只算一次并被每个守卫复用。这让守卫更易读。

haskell
-- multiple parameters
max' :: Int -> Int -> Int
max' a b
  | a > b     = a
  | otherwise = b

-- shared helpers via where
bmiTell :: Double -> Double -> String
bmiTell w h
  | bmi <= 18.5 = "underweight"
  | bmi <= 25.0 = "normal"
  | otherwise   = "overweight"
  where bmi = w / h^2

守卫与模式匹配

固定值与结构(字面量、构造器、列表)用模式匹配;布尔条件与范围用守卫。二者可结合:先匹配结构,再用守卫判断条件。

haskell
-- pattern matching: fixed values/structure
factorial 0 = 1
factorial n = n * factorial (n-1)

-- guards: ranges and conditions
clamp :: Int -> Int -> Int -> Int
clamp lo hi x
  | x < lo     = lo
  | x > hi     = hi
  | otherwise  = x

if-then-else

在 Haskell 中,if 是表达式,总返回一个值——两个分支都不可省。多路分支用守卫比嵌套 if-then-else 更清晰。if-then-else 适合简单的二选一。

haskell
-- 'if' is an expression that returns a value
abs' :: Int -> Int
abs' n = if n < 0 then -n else n

-- nested ifs (prefer guards for readability)
grade :: Int -> Char
grade n =
  if n >= 90 then 'A'
  else if n >= 80 then 'B'
  else if n >= 70 then 'C'
  else 'F'
08

Where / Let

where 子句

where 把定义附在函数上,作用域为其右侧(及守卫)。定义间可相互引用,顺序任意。很适合在守卫间共享辅助函数。

haskell
-- where: definitions at the bottom, scoped to a function
area :: Double -> Double -> Double
area w h = w * h_adj
  where
    h_adj = h * 0.9    -- adjust height
    scale = 0.9        -- a helper

-- definitions can refer to each other
f x = a + b
  where a = x * 2
        b = a + 1      -- refers to a

let-in 表达式

let ... in ... 是表达式,其值为 in 部分的值。绑定仅在该表达式内有效。与 where 不同,let 可出现在任何允许表达式的地方。

haskell
-- let <bindings> in <expr>
cylinder :: Double -> Double -> Double
cylinder r h =
  let side = 2 * pi * r * h
      top  = pi * r^2
  in  side + 2 * top

-- scoped to the expression only
> let z = 5 in z * z
25
-- z is not in scope here

do 与推导式中的 let

在 do 块和列表推导式内部,let 是语句,不需要 in。绑定对后续行或推导式元素有效。便于做局部计算。

haskell
-- in do, 'let' needs no 'in'
main = do
  let x = 5
      y = 10
  putStrLn $ "sum = " ++ show (x + y)

-- in a list comprehension, no 'in'
> [x*y | x <- [1..3], let y = x+1]
[2, 6, 12]

带模式的 where

where 绑定支持在 = 右侧进行模式匹配。便于解包函数体中反复用到的值。让 where 绑定聚焦于函数所需。

haskell
-- where bindings support pattern matching
describe :: [a] -> String
describe xs = "head is " ++ show first
  where (first:_) = xs

-- multiple where bindings
analyze :: [Int] -> String
analyze xs =
  "sum=" ++ show s ++ ", len=" ++ show l
  where s = sum xs
        l = length xs

let 与 where

let 是表达式(总返回值);where 是依附于定义的语法声明。内联局部计算用 let,守卫间共享辅助用 where。二者都是纯的且惰性求值。

haskell
-- let: an expression, available wherever expressions go
-- (needs 'in', except in do/comprehension)

-- where: a declaration, attached to a function
-- (cannot be nested as an expression)

-- same result, two styles:
-- with let:
f x = let y = x+1 in y*y

-- with where:
f' x = y*y where y = x+1
09

递归

基本递归

递归在 Haskell 中取代循环。定义基准情形(停止条件)和向其逼近的递归情形。编译器会把尾递归函数优化为循环。

haskell
-- base case + recursive case
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

> factorial 5
120

列表递归

地道的列表递归匹配 [](基准)与 (x:xs)(递归),处理 x 并对 xs 递归。许多列表递归用 fold 或 map 表达更好,但这一模式仍不可不学。

haskell
-- match [] then (x:xs)
length' :: [a] -> Int
length' []     = 0
length' (_:xs) = 1 + length' xs

sum' :: Num a => [a] -> a
sum' []     = 0
sum' (x:xs) = x + sum' xs

累加器(尾递归)

累加器携带中间结果,使递归成为尾递归——常量栈空间。常见做法是用一个名为 go 的辅助函数带累加器。GHC 会把它变成高效循环。

haskell
-- carry the running result in an accumulator
factorial :: Int -> Int
factorial n = go n 1
  where
    go 0 acc = acc
    go k acc = go (k-1) (k*acc)

-- sum with an accumulator
sumTo :: Int -> Int
sumTo n = go n 0
  where go 0 acc = acc
        go k acc = go (k-1) (k+acc)

快速排序

这个著名例子展示了 Haskell 的表现力:用列表推导式按主元划分,再递归排序两部分。并非最高效的快排,却是递归与推导式的绝佳演示。

haskell
-- elegant but not in-place quicksort
qsort :: Ord a => [a] -> [a]
qsort [] = []
qsort (p:xs) =
  qsort smaller ++ [p] ++ qsort larger
  where
    smaller = [x | x <- xs, x <  p]
    larger  = [x | x <- xs, x >= p]

> qsort [3,1,4,1,5,9,2,6]
[1,1,2,3,4,5,6,9]

相互递归

相互递归指两个或多个函数彼此调用。这里 even' 与 odd' 向基准情形 0 逼近。两者都必须定义(模块中顺序无关)。

haskell
-- functions calling each other
even' :: Int -> Bool
even' 0 = True
even' n = odd' (n - 1)

odd' :: Int -> Bool
odd' 0 = False
odd' n = even' (n - 1)

> even' 4
True
10

高阶函数

map

map :: (a -> b) -> [a] -> [b] 用函数变换每个元素,返回同长新列表。配合节与 lambda,可简洁表达许多循环。

haskell
-- map applies a function to every element
> map (*2) [1,2,3]
[2,4,6]

> map (+1) [1..5]
[2,3,4,5,6]

-- with a lambda
> map (\x -> x*x) [1..4]
[1,4,9,16]

-- strings are lists of chars
> map toUpper "hello"
"HELLO"

filter

filter :: (a -> Bool) -> [a] -> [a] 保留谓词为 True 的元素。filter 配 length 可统计匹配元素数。这是选取列表元素的惯用法。

haskell
-- keep elements satisfying a predicate
> filter even [1..10]
[2,4,6,8,10]

> filter (>3) [1,2,3,4,5]
[4,5]

-- with a lambda
> filter (\c -> c /= ' ') "hello world"
"helloworld"

-- count matches
> length (filter odd [1..100])
50

foldl 与 foldr

foldl 从左到右处理;foldr 从右到左。二者用二元函数和种子把列表归约为一个值。foldr 可处理无限列表(若惰性);foldl 不行。数值求和用 foldl'(严格)以避免空间泄漏。

haskell
-- foldl: left fold (function, seed, list)
> foldl (+) 0 [1..5]
15

-- foldr: right fold
> foldr (+) 0 [1..5]
15

-- associativity differs:
-- foldl: ((((0+1)+2)+3)+4)+5
-- foldr: 1+(2+(3+(4+(5+0))))

-- foldl1/foldr1 use the first element as seed
> foldl1 max [3,1,4,1,5]
5

zipWith

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] 把函数作用于两个列表的配对元素,到较短者为止。它是 zip 的推广。适合逐元素运算。

haskell
-- apply a function to paired elements
> zipWith (+) [1,2,3] [10,20,30]
[11,22,33]

> zipWith (*) [1,2,3,4] [2,2,2,2]
[2,4,6,8]

-- combine strings
> zipWith (\a b -> a ++ b) ["a","b"] ["1","2"]
["a1","b2"]

-- stops at the shorter list
> zipWith max [1,5,3] [2,4,6,8]
[2,5,6]

scanl 与 scanr

scanl/scanr 类似 foldl/foldr,但返回所有中间累加值的列表。适合累计求和、累计最大值等累积计算。

haskell
-- scanl returns all intermediate accumulators
> scanl (+) 0 [1,2,3]
[0,1,3,6]

-- scanr from the right
> scanr (+) 0 [1,2,3]
[6,5,3,0]

-- running maximum
> scanl max 0 [3,1,4,1,5]
[0,3,3,4,4,5]

takeWhile 与 dropWhile

takeWhile 在首个不满足谓词的元素处停止;dropWhile 去掉满足谓词的前缀。span 在首个不满足元素处切分列表。非常适合按前缀处理。

haskell
-- takeWhile: take while the predicate holds
> takeWhile (<5) [1,2,3,4,5,4,3]
[1,2,3,4]

-- dropWhile: drop while the predicate holds
> dropWhile (<5) [1,2,3,4,5,4,3]
[5,4,3]

-- span splits at the first failing element
> span (<3) [1,2,3,4,5]
([1,2],[3,4,5])

-- first word of a string
> takeWhile (/= ' ') "hello world"
"hello"
11

Lambda

匿名函数

lambda 是匿名函数:\args -> body。反斜杠形似希腊字母 λ。用于传给高阶函数的短小一次性函数,免得起名。

haskell
-- '\' defines a lambda (resembles the λ symbol)
> (\x -> x + 1) 5
6

-- used with map
> map (\x -> x * x) [1..4]
[1,4,9,16]

-- used with filter
> filter (\x -> x > 2) [1,2,3,4]
[3,4]

多参数 lambda

lambda 的参数用空格分隔,与普通函数定义一致:\x y -> .... 与所有 Haskell 函数一样,lambda 是柯里化的——一次一个参数。

haskell
-- parameters separated by spaces
> (\x y -> x + y) 3 4
7

-- in zipWith
> zipWith (\x y -> x * y + 1) [1,2,3] [10,20,30]
[11,21,31]

带模式的 lambda

lambda 可对其唯一参数进行模式匹配,如 \(a,b) -> .... 但 lambda 只允许一个模式且无兜底,不匹配的输入会崩溃。多模式请定义具名函数。

haskell
-- lambdas can pattern match their one argument
> map (\(a,b) -> a + b) [(1,2),(3,4)]
[3,7]

-- only ONE pattern, no fallthrough
-- this crashes if it doesn't match:
> (\(x:_) -> x) []   -- runtime error

高阶函数中的 lambda

lambda 在 fold、filter、map 中大放异彩。它们可返回其他函数(\x -> \y -> ...),体现柯里化。保持简短——过长的 lambda 用具名函数更清晰。

haskell
-- combining with fold
> foldl (\acc x -> acc + x*2) 0 [1,2,3]
12

-- with filter (infix via backticks)
> length . filter (\x -> x `mod` 2 == 0) $ [1..10]
5

-- a lambda returning a function
> (\x -> \y -> x + y) 3 4
7

无参风格

无参风格省略显式参数,用组合与应用表达函数。简洁但过度使用会损害可读性。流向明显时适合用短链。

haskell
-- explicit: name every argument
sumSquares xs = sum (map (^2) xs)

-- point-free: omit arguments via composition
sumSquares' = sum . map (^2)

-- more examples
count p  = length . filter p
oddNums  = filter odd
negateAll = map negate
12

柯里化

部分应用

每个多参函数其实都是单参函数的链(柯里化)。提供少于所需数量的参数会得到一个新函数(部分应用)。这能通过函数工厂实现强大复用。

haskell
-- all functions are curried (one arg at a time)
add :: Int -> Int -> Int
add x y = x + y

-- apply fewer args to get a function
add5 :: Int -> Int
add5 = add 5

> add5 10
15

-- with map
> map (add 5) [1,2,3]
[6,7,8]

节部分应用中缀运算符:(+3)、(3/)、(>3)。注意 (-3) 是数字负三,不是节——请用 (subtract 3)。节常与 map、filter 搭配。

haskell
-- left section (fix the left operand)
> map (+3) [1,2,3]
[4,5,6]

-- right section (fix the right operand)
> map (3/) [9, 12, 18]
[0.333..,0.25,0.166..]

-- (-3) is the NUMBER, not a section!
> map (subtract 3) [4,5,6]
[1,2,3]

-- comparison sections
> filter (>3) [1..6]
[4,5,6]

flip

flip 取一个二元函数,返回前两个参数交换后的函数。当你想固定第二个参数而非第一个时,部分应用很方便。

haskell
-- flip swaps the first two arguments
flip :: (a -> b -> c) -> b -> a -> c
flip f x y = f y x

-- fix the second argument instead of the first
> map (flip (-) 1) [3,4,5]
[2,3,4]

-- divide from the other side
> map (flip div 2) [4,6,9]
[2,3,4]

柯里化解析

a -> b -> c 表示 a -> (b -> c):取 a 并返回一个函数。应用是左结合的:f x y = (f x) y。这就是部分应用自然生效的原因。

haskell
-- this signature:
max :: Int -> Int -> Int

-- is actually: Int -> (Int -> Int)
-- max takes an Int and returns a function

-- step by step
> :t max 5
max 5 :: Int -> Int

> (max 5) 10
10

-- application is left-associative
> max 5 10 == ((max 5) 10)
True

返回函数的函数

函数可以返回其他函数,这自然源于柯里化。这造就了函数工厂:multiplyBy 2 创建一个加倍函数。像 greet "Hi" 这样的部分应用可配置稍后使用的函数。

haskell
-- return a function
multiplyBy :: Int -> (Int -> Int)
multiplyBy n = \x -> n * x

-- or equivalently (currying)
multiplyBy' n x = n * x

> let double = multiplyBy 2
> double 21
42

-- configure a function via partial application
greet :: String -> String -> String
greet greeting name = greeting ++ ", " ++ name
sayHi = greet "Hi"   -- a configured greeter
13

类型类

基本类型类

类型类定义共享接口。Eq 提供 == 与 /=;Ord 提供 <、>、compare;Show 提供 show;Read 提供 read。类型必须实现类型类才能用其方法。

haskell
-- Eq: equality
> 5 == 5
True
> "a" /= "b"
True

-- Ord: ordering
> 3 < 5
True
> compare 3 5
LT

-- Show: convert to a string
> show 5
"5"
> show [1,2,3]
"[1,2,3]"

-- Read: parse from a string
> read "5" + 2
7

类型约束

=> 引入类型约束:左侧类型必须满足它们。Eq a => 表示 a 必须属于 Eq 类型类。多个约束用括号与逗号分隔。

haskell
-- '=>' constrains the type variables
(==) :: Eq a => a -> a -> Bool
(<)  :: Ord a => a -> a -> Bool
show :: Show a => a -> String

-- a function requiring Eq
elem :: Eq a => a -> [a] -> Bool
elem _ []     = False
elem x (y:ys) = x == y || elem x ys

-- multiple constraints
showMax :: (Ord a, Show a) => [a] -> String
showMax xs = show (maximum xs)

定义类型类

class Name a where 声明带方法签名的类型类。默认实现让实例只定义最小集——其余自动派生。这里定义 == 即免费得到 /=,反之亦然。

haskell
-- declare a typeclass with method signatures
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  -- default implementations
  x == y = not (x /= y)
  x /= y = not (x == y)

-- minimal: define either (==) or (/=)

实例声明

instance Typeclass Type where 为某类型实现类型类。除非有默认实现,每个方法都要定义。之后 TrafficLight 值即可使用 ==、/= 和 show。

haskell
data TrafficLight = Red | Yellow | Green

instance Eq TrafficLight where
  Red == Red         = True
  Yellow == Yellow   = True
  Green == Green     = True
  _ == _             = False

instance Show TrafficLight where
  show Red    = "Red light"
  show Yellow = "Yellow light"
  show Green  = "Green light"

数值类型类

数值层级包括 Num(所有数)、Fractional(支持 /)、Integral(支持 div、mod)。Int 与 Double 是不同类型——用 fromIntegral 在它们之间安全转换。

haskell
-- Num: basic numeric operations
(+) :: Num a => a -> a -> a
(*) :: Num a => a -> a -> a
negate :: Num a => a -> a

-- Fractional: supports division
(/) :: Fractional a => a -> a -> a

-- Integral: integer division
div, mod :: Integral a => a -> a -> a

-- convert between numeric types
> fromIntegral (length [1,2,3]) + 0.5
3.5

派生

许多类型类可自动派生:Eq、Ord、Show、Read、Enum、Bounded。这省去样板实例。Enum 与 Bounded 启用范围语法([Red ..])和 minBound/maxBound。

haskell
-- automatically derive common typeclasses
data Color = Red | Green | Blue
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

> show Red
"Red"
> Red < Green
True
> minBound :: Color
Red
> [Red ..]
[Red,Green,Blue]
14

代数数据类型

data 关键字

data 定义代数数据类型。用 | 分隔的分支构成和类型;每个分支是值构造器。构造器是产生该类型值的函数。模式匹配用于解构。

haskell
-- sum type: alternatives separated by '|'
data Bool' = False' | True'

-- pattern match on constructors
not' :: Bool' -> Bool'
not' True'  = False'
not' False' = True'

-- value constructors with no fields
data Weekday = Mon | Tue | Wed | Thu | Fri

带字段的构造器

构造器可携带字段。Point 有两个 Double 字段;Shape 有两个不同字段的分支。构造器是函数:Circle :: Double -> Shape。类型与构造器可同名。

haskell
-- product type: constructors carry fields
data Point = Point Double Double

data Shape
  = Circle Double         -- radius
  | Rect Double Double    -- width, height

-- construct values
p = Point 1.0 2.0
c = Circle 5.0
r = Rect 3.0 4.0

参数化类型

类型参数使类型泛型化。Maybe' a 存放任意类型 a 的值或 Nothing'。参数让一个定义服务 Int、String 等。Maybe 是建模可选值的标准方式。

haskell
-- a type parameter 'a' (like Maybe)
data Maybe' a = Nothing' | Just' a

-- a boxed value
data Box a = Box a

-- use it with any type
> Just' 5
Just' 5
> Box "hello"
Box "hello"

-- model failure with Maybe
divide :: Double -> Double -> Maybe' Double
divide _ 0 = Nothing'
divide x y = Just' (x/y)

递归类型

类型可以引用自身,形成递归结构。IntList 是 Int 的单链表。Tree a 是二叉树。模式匹配自然地在此类结构上递归。

haskell
-- a custom linked list
data IntList = INil | ICons Int IntList

-- build the list 1,2,3
xs = ICons 1 (ICons 2 (ICons 3 INil))

-- a binary tree
data Tree a = Leaf | Node a (Tree a) (Tree a)

tree = Node 1 (Node 2 Leaf Leaf) (Node 3 Leaf Leaf)

带函数的二叉搜索树

代数数据类型与通过模式匹配的递归函数天然契合。这个 BST 实现插入时保持顺序,中序遍历得到有序列表。ADT 加递归取代了 OOP 中的类。

haskell
data Tree a = Leaf | Node a (Tree a) (Tree a)

-- insert into a binary search tree
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node x Leaf Leaf
insert x (Node y l r)
  | x < y     = Node y (insert x l) r
  | otherwise = Node y l (insert x r)

-- inorder traversal yields a sorted list
inorder :: Tree a -> [a]
inorder Leaf         = []
inorder (Node x l r) = inorder l ++ [x] ++ inorder r

枚举

由无参构造器组成的和类型类似枚举。派生 Enum 启用 succ/pred 与范围语法([North ..]);派生 Bounded 给出 minBound/maxBound。这是 Haskell 地道的枚举方式。

haskell
-- enum-like sum type
data Direction = North | East | South | West
  deriving (Eq, Ord, Enum, Bounded, Show)

-- enumerate via Enum
> [North .. West]
[North,East,South,West]

-- Bounded: the limits
> minBound :: Direction
North
> maxBound :: Direction
West

-- cycle through directions
next :: Direction -> Direction
next West = North
next d    = succ d
15

记录

记录语法

记录语法为每个字段命名,比位置构造器更可读。类型与构造器可同名。字段顺序无关——可按任意顺序创建值。

haskell
-- without records (positional, error-prone)
data Person0 = Person0 String Int String

-- with records (named fields)
data Person = Person
  { name :: String
  , age  :: Int
  , city :: String
  }

-- construct with field names (any order)
p1 = Person { name = "Alice", age = 30, city = "NYC" }

访问字段

每个字段名成为访问函数:name :: Person -> String。也可用记录语法模式匹配(Person { name = n })。访问函数是普通函数,可与组合搭配。

haskell
-- field names are accessor functions
> name p1
"Alice"
> age p1
30

-- their types
> :t name
name :: Person -> String
> :t age
age :: Person -> Int

-- record patterns also work
greet :: Person -> String
greet (Person { name = n }) = "Hi, " ++ n

更新记录

记录更新创建一个新值,只改指定字段,其余不变:record { field = newVal }。原值不可变——更新返回副本。可一次改多个字段。

haskell
-- functional update: change only some fields
p2 = p1 { age = 31 }

-- the original is unchanged (immutable)
> age p1
30
> age p2
31

-- update multiple fields at once
p3 = p1 { age = 31, city = "LA" }

记录模式

记录模式按名绑定字段,忽略未指定者。配合 as 模式(p@Person{...})可同时保留整个记录并选取字段。避免脆弱的位置匹配。

haskell
-- match and bind fields by name
birthday :: Person -> Person
birthday p@Person { age = a } = p { age = a + 1 }

-- ignore unneeded fields
getName :: Person -> String
getName Person { name = n } = n

-- as-pattern keeps the whole record
isAdult :: Person -> Bool
isAdult p@Person { age = a } = a >= 18

字段命名注意事项

Haskell 的记录字段名位于顶层命名空间,所以同一模块中两个记录不能共享同名字段。常见变通是字段加前缀(personName)或使用 DuplicateRecordFields 扩展。

haskell
-- field names share a top-level namespace
data Person  = Person  { name :: String }
data Company = Company { name :: String }
-- ERROR: 'name' clashes in the same module

-- workaround 1: prefix the fields
data Person'  = Person'  { personName :: String }
data Company' = Company' { companyName :: String }

-- workaround 2: language extensions
{-# LANGUAGE DuplicateRecordFields #-}
16

模块

定义模块

模块以 module Name (exports) where 开头。导出列表控制可见性——省略则全部导出。未导出的定义相当于私有辅助。

haskell
-- file Geometry.hs
module Geometry
  ( sphereVolume    -- explicit export list
  , cubeVolume
  ) where

sphereVolume :: Float -> Float
sphereVolume r = (4/3) * pi * r^3

cubeVolume :: Float -> Float
cubeVolume s = s^3

-- a helper not in the export list is private
privateHelper = ...

导入模块

import Module 把名字引入作用域。选择性导入(import M (f, g))限制导入内容。限定导入(import qualified M as M)要求 M. 前缀,避免名字冲突。

haskell
-- import everything from Data.List
import Data.List

-- names are now in scope
> sort [3,1,2]
[1,2,3]
> nub [1,1,2,2,3]
[1,2,3]

-- import specific functions
import Data.Maybe (fromMaybe)

-- qualified: use the full module name
import qualified Data.Map as M
> M.empty

选择性与隐藏导入

选择性导入(import M (a, b))只引入列出的名字。hiding 导入除列出名字之外的所有内容。用它们保持命名空间整洁,避免意外遮蔽 Prelude 函数。

haskell
-- import only specific names
import Data.List (sort, nub, group)

-- import everything except some
import Data.List hiding (head, tail)

-- import a type without its constructors
import Data.Tree (Tree, Node)

-- import constructors too
import Data.Tree (Tree(..))

限定导入

限定导入强制加模块前缀,避免冲突(如 Prelude 的 filter 与 Data.Map 的)。import qualified M as M 是惯用法。对名字重叠多的模块用限定导入。

haskell
-- avoid name clashes with qualified
import qualified Data.Map as Map
import qualified Data.Set as Set

m = Map.empty
s = Set.fromList [1,2,3]

-- Prelude's filter vs Data.Map's
> filter even [1..6]
[2,4,6]
> Map.filter (>2) (Map.fromList [(1,'a'),(3,'b')])
fromList [(3,'b')]

Prelude

Prelude 是默认导入,提供常用函数与类型。隐藏条目可避免冲突或使用更好替代(如 Data.List 的 foldl')。很多项目用自定义 Prelude。

haskell
-- Prelude is imported by default
-- provides: map, filter, length, (+), etc.

-- hide entries to avoid clashes
import Prelude hiding (head, tail, foldl)

-- use better alternatives
import Data.List (foldl')   -- strict fold

-- common standard modules
import Data.List
import Data.Maybe
import Data.Char
import Data.Either

模块层级

模块名分层并对应目录结构:A.B.C 位于 A/B/C.hs。模块可在导出列表中用 module M 再导出另一模块,便于打包公共 API。

haskell
-- hierarchical names mirror directories
-- module Data.Geometry.Sphere where
-- lives in: Data/Geometry/Sphere.hs

-- import nested modules
import Data.Geometry.Sphere
import Data.Geometry.Cube

-- re-export a whole module
module MyMod
  ( module Data.List    -- re-export
  , myFunc
  ) where

import Data.List
myFunc = sort
17

IO Monad

基本 IO

IO 动作类型为 IO a——描述被运行时(由 main 或 GHCi)执行的副作用。do 串行动作;<- 从动作提取值。IO 类型让副作用显式且隔离。

haskell
-- IO actions produce effects when run
greet :: IO ()
greet = do
  putStrLn "What's your name?"
  name <- getLine
  putStrLn ("Hello, " ++ name)

-- run in GHCi
> greet
What's your name?
Alice
Hello, Alice

main 函数

main :: IO () 是程序入口。do 顺序组合 IO 动作。getArgs 提供命令行参数。纯代码可在 do 中直接调用;结果用 <- 绑定。

haskell
-- every program starts in main
main :: IO ()
main = putStrLn "Hello, World!"

-- combine actions with do
main = do
  greet
  putStrLn "Done."

-- command-line arguments
import System.Environment
main = do
  args <- getArgs
  print args

do 记法

do 是 monadic 绑定 (>>=) 与 (>>) 的语法糖。每个 x <- action 变成 action >>= \x -> .... do 内的 let 不需要 in。do 让顺序 IO 自上而下易读。

haskell
-- do is sugar for sequential actions
echo = do
  s <- getLine
  putStrLn s

-- desugars to bind (>>=)
echo' = getLine >>= putStrLn

-- 'let' needs no 'in' inside do
greet = do
  let msg = "Hi!"
  putStrLn msg

输入函数

getLine 读一行(不含换行),getChar 读单字符,getContents 惰性读取全部 stdin。用 <- 绑定结果。getContents 是惰性的——很适合流式管道。

haskell
-- read a line (without the newline)
> line <- getLine
hello world
> line
"hello world"

-- read a single character
> c <- getChar
a> c
'a'

-- read all of stdin lazily
process = do
  text <- getContents
  putStr (map toUpper text)

输出函数

putStrLn 与 putStr 把字符串写入 stdout,前者带换行。print 等于 putStrLn . show,所以会给字符串加引号。按是否需要换行、值是否为字符串来选择。

haskell
-- with a trailing newline
putStrLn :: String -> IO ()
-- without a newline
putStr   :: String -> IO ()
-- any Show-able value
print    :: Show a => a -> IO ()

> putStrLn "hi"   -- hi
> putStr "hi"     -- hi (no newline)
> print 5         -- 5
> print "hi"      -- "hi" (with quotes)

文件 IO

readFile 惰性读取,writeFile 覆写,appendFile 追加到末尾。用 bracket 确保异常时资源关闭。文件以 FilePath(String 别名)寻址。

haskell
-- read a whole file (lazy)
readFile :: FilePath -> IO String

-- write a file (overwrites)
writeFile :: FilePath -> String -> IO ()

-- append to a file
appendFile :: FilePath -> String -> IO ()

main = do
  contents <- readFile "input.txt"
  writeFile "output.txt" (map toUpper contents)

-- safe resource handling
import Control.Exception (bracket)
18

Functor / Applicative / Monad

Functor

Functor 是可被映射的上下文。fmap(或 <$>)把函数作用于被包裹的值,保持结构不变。Maybe、List、IO 和函数都是 Functor。

haskell
-- Functor: a context you can map over
class Functor f where
  fmap :: (a -> b) -> f a -> f b

-- '<$>' is fmap as an operator
> fmap (+1) (Just 5)
Just 6
> (+1) <$> Just 5
Just 6
> (+1) <$> Nothing
Nothing
> map (+1) [1,2,3]   -- map is fmap for lists
[2,3,4]

Applicative

Applicative 把多参函数提升进上下文。pure 放入值,<*> 把被包裹的函数作用于被包裹的值。对列表,<*> 产生应用的笛卡尔积。

haskell
-- Applicative: functions inside a context
class Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

-- apply a wrapped function
> pure (+) <*> Just 3 <*> Just 4
Just 7
> Just (+1) <*> Just 5
Just 6

-- for lists, <*> is the Cartesian product
> [(+1), (*2)] <*> [1,2,3]
[2,3,4,2,4,6]

Monad 与 bind

Monad 支持链接依赖前序结果的计算。>>=(bind)把被包裹的值喂给一个返回新包裹值的函数。return(或 pure)注入值。Monad 统一了顺序、失败与状态。

haskell
-- Monad: sequential, dependent computations
class Monad m where
  return :: a -> m a
  (>>=)  :: m a -> (a -> m b) -> m b   -- bind

-- bind chains dependent computations
> Just 3 >>= \x -> Just (x+1)
Just 4
> Nothing >>= \x -> Just (x+1)
Nothing

-- '>>' ignores the left result
> putStrLn "a" >> putStrLn "b"
a
b

do 脱糖

do 是 (>>=) 与 let 绑定的语法糖。每个 x <- action 翻译为 action >>= \x -> .... 理解脱糖能揭开 do 的神秘,并说明 monad 如何支撑它。

haskell
-- do notation
greet = do
  name <- getLine
  putStrLn ("Hi " ++ name)

-- desugars to bind
greet' = getLine >>= \name ->
         putStrLn ("Hi " ++ name)

-- 'let' becomes a regular binding
compute = do
  let x = 5
  print (x * x)
-- desugars to:
compute' = let x = 5 in print (x*x)

Maybe Monad 链

Maybe 的 Monad 会短路:一旦某步返回 Nothing,整个计算即为 Nothing,不再继续。do 隐藏了手动的 Nothing 检查,让失败传播自动且干净。

haskell
-- short-circuit on Nothing
safeDiv :: Double -> Double -> Maybe Double
safeDiv _ 0 = Nothing
safeDiv x y = Just (x / y)

-- chain with do (auto-propagates failure)
calc = do
  a <- safeDiv 10 2
  b <- safeDiv a 2
  return (b + 1)

> calc
Just 3.5

-- any Nothing stops the whole chain
> safeDiv 10 0 >>= \a -> safeDiv a 2
Nothing

List Monad

列表 monad 建模非确定性:每次 bind 探索所有可能,产生笛卡尔积。列表上的 do 等价于列表推导式。这统一了推导式与 monadic 风格。

haskell
-- list bind = non-deterministic computation
> [1,2] >>= \x -> [x, x*10]
[1,10,2,20]

-- do over lists == list comprehension
pairs = do
  x <- [1,2]
  y <- ['a','b']
  return (x, y)

> pairs
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

-- equivalent comprehension
> [(x,y) | x <- [1,2], y <- ['a','b']]
19

类型别名

type 别名

type 创建别名——现有类型的另一名字。无运行时开销,也不提供类型安全(PhoneNumber 与 String 可互换)。用它让签名更可读。

haskell
-- 'type' creates a synonym (not a new type)
type String = [Char]

type PhoneNumber = String
type Name = String

-- improves readability of signatures
printEntry :: Name -> PhoneNumber -> IO ()
printEntry name num = putStrLn (name ++ ": " ++ num)

newtype

newtype 用单字段构造器包裹现有类型,在编译期创建独立类型且无运行时开销(会被擦除)。与 type 不同,它阻止 Dollars 与普通 Double 混用——真正的类型安全。

haskell
-- newtype: one constructor, one field, zero cost
newtype Dollars = Dollars Double
newtype Count   = Count Int

-- pattern match or unwrap the field
addDollars :: Dollars -> Dollars -> Dollars
addDollars (Dollars a) (Dollars b) = Dollars (a + b)

-- distinct type: can't mix with raw Double
total :: Dollars
total = addDollars (Dollars 10) (Dollars 20)

type、newtype 与 data

type 是别名(可互换、无安全)。newtype 是零开销的独立包装(一个构造器、一个字段)。data 是完整代数类型(多构造器/字段、有运行时开销)。要类型安全又无开销就选 newtype。

haskell
-- type: just a synonym (no new type, no safety)
type Score = Int

-- newtype: distinct type, zero cost, one field
newtype Score' = Score' Int

-- data: full ADT, runtime cost,
-- can have multiple constructors/fields
data Score'' = Score'' Int | Bonus Int

-- newtype is strict: exactly one
-- constructor and one field, but a distinct type

用 newtype 添加新实例

newtype 让你为同一底层类型附加不同的类型类实例。Sum 与 Product 包裹同样的数,却有不同的 Monoid 实例(加法与乘法)。常见且强大的模式。

haskell
-- wrap to attach different typeclass instances
newtype Sum a = Sum { getSum :: a }

instance Num a => Monoid (Sum a) where
  mempty = Sum 0
  Sum a `mappend` Sum b = Sum (a + b)

newtype Product a = Product { getProduct :: a }
-- different Monoid: multiplication

-- same underlying type, different behavior
> getSum (Sum 3 <> Sum 4)
7
> getProduct (Product 3 <> Product 4)
12

参数化别名

类型别名可带参数,作为轻量抽象。AssocList k v 即 [(k,v)],让签名自我说明。为清晰使用它们,但记住它们只是别名,无类型安全。

haskell
-- type synonyms can take parameters
type AssocList k v = [(k, v)]

-- used in signatures (self-documenting)
lookup :: Eq k => k -> AssocList k v -> Maybe v
lookup _ [] = Nothing
lookup k ((k',v):rest)
  | k == k'   = Just v
  | otherwise = lookup k rest

-- more examples
type Parser a = String -> Maybe (a, String)
type Matrix a = [[a]]

这篇内容对您有帮助吗?