入门
基础与 GHCi
Haskell 是纯函数式语言。GHCi 是交互式 REPL。函数应用不需要括号——succ 5 等同于其他语言中的 succ(5)。字符串只是字符的列表。
-- 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 的注释可以嵌套,注释掉已含注释的代码时很方便。
-- single line comment
{- multi-line comment
spanning multiple lines -}
{- nested {- comments -} are allowed -}
-- comments are ignored by the compilerHello World
main 是每个 Haskell 程序的入口。putStrLn 输出字符串并换行。用 ghc 编译为原生可执行文件,或用 runhaskell 解释运行。
-- hello.hs
main :: IO ()
main = putStrLn "Hello, World!"
-- run interpreted
-- runhaskell hello.hs
-- or compile to an executable
-- ghc hello.hs && ./helloGHCi 中的 let
在 GHCi 中,let 定义绑定不需要 in。在源文件中,顶层定义不使用 let,而表达式内的 let 需要配对的 in。这一差异常让初学者困惑。
-- 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 命令是探索陌生代码的利器。
-- :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 工作流很顺滑。
-- 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类型
基本类型
Int 是定长整数;Integer 无界,可按需增长。为精度优先用 Double 而非 Float。Char 存放单个 Unicode 字符。用类型标注让类型更明确。
-- 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'类型标注
:: 为表达式或绑定添加类型标注。类型签名通常位于定义上方。它们提升可读性并让编译器更早发现错误。
-- 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]类型变量
类型签名中的小写标识符是类型变量——可代表任意类型,使函数成为多态的。同一个变量在一条签名中必须指代同一类型。
-- '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)。最后一个类型是返回类型,其余是参数类型。函数可以接受或返回其他函数。
-- '->' 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 = xMaybe 与 Either
Maybe 表示可能失败的计算(Just 值或 Nothing)。Either 携带错误信息(Left 错误或 Right 值)。两者都替代 null 与异常,实现更安全的错误处理。
-- 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] 存放任意数量、同一类型的值。元组的类型编码了其大小;列表的类型则不。
-- 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 = []函数
函数定义
函数由名称、参数和 = 定义。应用只需一个空格——无需括号或逗号。add 3 4 实际上是 ((add 3) 4),这是柯里化的结果。
-- 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函数应用与 $
应用比运算符结合更紧,且是左结合的。$ 是低优先级、右结合的应用运算符,用于省略最右侧参数周围的括号。
-- 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 更自然。
-- 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)。它支持无参风格,省略参数。链式调用从内向外读,但避免了层层括号。
-- (.) 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 接受一个函数并应用两次。这是高阶编程的 基础。
-- 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)。按可读性选择即可。
-- 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列表
列表基础
列表是同类型元素的单链表。字符串就是 [Char]。:(cons)以 O(1) 在头部插入。列表不可变——操作返回新列表。
-- 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 取出有限前缀。
-- 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)。
-- 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 用分隔符连接列表。递归构建列表时优先用 :。
-- '++' 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 定义辅助值。
-- 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 按索引 切分成一对。因列表不可变,都返回新列表。
-- 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])元组
元组基础
元组将固定数量、可能不同类型的值组合在一起。分量数是类型的一部分——(Int,Int) 与 (Int,Int,Int) 是不同类型。元组不可变。
-- 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。
-- 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 表达式。
-- 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 用函数合并配对元素,而非组成元组。
-- 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")元组与列表
用列表存放同类型的变长序列,用元组存放定长的异构组合。列表长度不在类型中;元组元数在类型中。元组列表常用于表格数据。
-- 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模式匹配
基本模式匹配
模式匹配自上而下尝试,第一个匹配者胜出。字面量模式匹配精确值。_ 是匹配任意值的通配符。务必加一个兜底分支,避免非穷尽匹配错误。
-- 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:_) 绑定前两个元素。这些模式是递归处理列表的基础。
-- 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元组模式
元组模式按位置绑定分量。用 _ 忽略不需要的分量。嵌套元组需要嵌套模式。这是解包元组值的惯用法。
-- 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, _) = nameAs 模式
As 模式(name@pattern)在解构的同时把整个匹配值绑定到一个名字。当你同时需要原值与其组成部分时很有用,避免昂贵的重建。
-- '@' 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 在表达式内进行模式匹配。当函数体需要在某些设置之后对某值分支时很有用。模式按顺序尝试;需要时可加兜底。
-- 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。这是代数数据类型的解构方式。编译器会对非穷尽模式发出警告,所以请覆盖所有构造器。
-- 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守卫
基本守卫
守卫按顺序测试布尔条件;第一个为 True 的胜出。otherwise 定义为 True,作为兜底。对于多路分支,守卫比 if-then-else 更清晰。
-- '|' 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 结尾。
-- 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 只算一次并被每个守卫复用。这让守卫更易读。
-- 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守卫与模式匹配
固定值与结构(字面量、构造器、列表)用模式匹配;布尔条件与范围用守卫。二者可结合:先匹配结构,再用守卫判断条件。
-- 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 = xif-then-else
在 Haskell 中,if 是表达式,总返回一个值——两个分支都不可省。多路分支用守卫比嵌套 if-then-else 更清晰。if-then-else 适合简单的二选一。
-- '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'Where / Let
where 子句
where 把定义附在函数上,作用域为其右侧(及守卫)。定义间可相互引用,顺序任意。很适合在守卫间共享辅助函数。
-- 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 alet-in 表达式
let ... in ... 是表达式,其值为 in 部分的值。绑定仅在该表达式内有效。与 where 不同,let 可出现在任何允许表达式的地方。
-- 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 heredo 与推导式中的 let
在 do 块和列表推导式内部,let 是语句,不需要 in。绑定对后续行或推导式元素有效。便于做局部计算。
-- 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 绑定聚焦于函数所需。
-- 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 xslet 与 where
let 是表达式(总返回值);where 是依附于定义的语法声明。内联局部计算用 let,守卫间共享辅助用 where。二者都是纯的且惰性求值。
-- 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递归
基本递归
递归在 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 表达更好,但这一模式仍不可不学。
-- 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 会把它变成高效循环。
-- 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 的表现力:用列表推导式按主元划分,再递归排序两部分。并非最高效的快排,却是递归与推导式的绝佳演示。
-- 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 逼近。两者都必须定义(模块中顺序无关)。
-- 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高阶函数
map
map :: (a -> b) -> [a] -> [b] 用函数变换每个元素,返回同长新列表。配合节与 lambda,可简洁表 达许多循环。
-- 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 可统计匹配元素数。这是选取列表元素的惯用法。
-- 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])
50foldl 与 foldr
foldl 从左到右处理;foldr 从右到左。二者用二元函数和种子把列表归约为一个值。foldr 可处理无限列表(若惰性);foldl 不行。数值求和用 foldl'(严格)以避免空间泄漏。
-- 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]
5zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] 把函数作用于两个列表的配对元素,到较短者为止。它是 zip 的推广。适合逐元素运算。
-- 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,但返回所有中间累加值的列表。适合累计求和、累计最大值等累积计算。
-- 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 在首个不满足元素处切分列表。非常适合按前缀处理。
-- 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"Lambda
匿名函数
lambda 是匿名函数:\args -> body。反斜杠形似希腊字母 λ。用于传给高阶函数的短小一次性函数,免得起名。
-- '\' 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 是柯里化的——一次一个参数。
-- 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 只允许一个模式且无兜底,不匹配的输入会崩溃。多模式请定义具名函数。
-- 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 用具名函数更清晰。
-- 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无参风格
无参风格省略显式参数,用组合与应用表达函数。简洁但过度使用会损害可读性。流向明显时适合用短链。
-- 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柯里化
部分应用
每个多参函数其实都是单参函数的链(柯里化)。提供少于所需数量的参数会得到一个新函数(部分应用)。这能通过函数工厂实现强大复用。
-- 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 搭配。
-- 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 取一个二元函数,返回前两个参数交换后的函数。当你想固定第二个参数而非第一个时,部分应用很方便。
-- 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。这就是部分应用自然生效的原因。
-- 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" 这样的部分应用可配置稍后使用的函数。
-- 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类型类
基本类型类
类型类定义共享接口。Eq 提供 == 与 /=;Ord 提供 <、>、compare;Show 提供 show;Read 提供 read。类型必须实现类型类才能用其方法。
-- 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 类型类。多个约束用括号与逗号分隔。
-- '=>' 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 声明带方法签名的类型类。默认实现让实例只定义最小集——其余自动派生。这里定义 == 即免费得到 /=,反之亦然。
-- 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。
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 在它们之间安全转换。
-- 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。
-- 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]代数数据类型
data 关键字
data 定义代数数据类型。用 | 分隔的分支构成和类型;每个分支是值构造器。构造器是产生该类型值的函数。模式匹配用于解构。
-- 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。类型与构造器可同名。
-- 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 是建模可选值的标准方式。
-- 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 是二叉树。模式匹配自然地在此类结构上递归。
-- 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 中的类。
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 地道的枚举方式。
-- 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记录
记录语法
记录语法为每个字段命名,比位置构造器更可读。类型与构造器可同名。字段顺序无关——可按任意顺序创建值。
-- 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 })。访问函数是普通函数,可与组合搭配。
-- 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 }。原值不可变——更新返回副本。可一次改多个字段。
-- 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{...})可同时保留整个记录并选取字段。避免脆弱的位置匹配。
-- 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 扩展。
-- 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 #-}模块
定义模块
模块以 module Name (exports) where 开头。导出列表控制可见性——省略则全部导出。未导出的定义相当于私有辅助。
-- 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. 前缀,避免名字冲突。
-- 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 函数。
-- 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 是惯用法。对名字重叠多的模块用限定导入。
-- 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。
-- 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。
-- 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 = sortIO Monad
基本 IO
IO 动作类型为 IO a——描述被运行时(由 main 或 GHCi)执行的副作用。do 串行动作;<- 从动作提取值。IO 类型让副 作用显式且隔离。
-- 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, Alicemain 函数
main :: IO () 是程序入口。do 顺序组合 IO 动作。getArgs 提供命令行参数。纯代码可在 do 中直接调用;结果用 <- 绑定。
-- 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 argsdo 记法
do 是 monadic 绑定 (>>=) 与 (>>) 的语法糖。每个 x <- action 变成 action >>= \x -> .... do 内的 let 不需要 in。do 让顺序 IO 自上而下易读。
-- 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 是惰性的——很适合流式管道。