Haskell 简明教程
Haskell 的语法风格与传统的 C 族语言差别很大。程序由表达式构成,没有命令式语言中的“变量赋值”与“语句顺序执行”概念。
-- 这是单行注释
{-
这是多行块级注释。
{- 多行注释支持嵌套 -}
-}
与 Python 类似,Haskell 使用缩进而非大括号与分号来确定代码块的边界。这套机制在语言规范中被称为越界规则(Offside Rule):
同级对齐原则:处于同一个逻辑块(如 where、let 或 do)内的多个声明,必须保持相同的起始列缩进。
续行缩进原则:如果一个长表达式拆分为多行,后续行必须比起始声明行缩进更深。
不要使用 Tab:不同编辑器对 Tab 宽度的解释不同,容易让编译器解析出错误的缩进。把编辑器配置为用空格缩进(通常为 2 格或 4 格)。
-- 正确对齐示例:
calculateValues :: Double -> (Double, Double)
calculateValues x = (firstVal, secondVal)
where
firstVal = x * 2 + 1 -- 同列对齐
secondVal = x ^ 2 + 3
在 Haskell 中,所有数据一旦创建就不可变。我们通过名称将表达式绑定到某个标识符:
x = 10
y = 20
z = x + y
这里的 = 代表数学意义上的定义与命名绑定,而不是内存地址的“覆写赋值”。在同一作用域内,同一个名称不能被重复绑定。
常用的基本算术运算符包括:
加法:+
减法:-
乘法:*
浮点除法:/(例如 7 / 2 结果为 3.5)
整数整除:div
整数取模:mod
ghci> 7 / 2
3.5
ghci> div 7 2
3
ghci> mod 7 2
1
Haskell 中的 - 既是二元减法运算符,也是一元负号。为了避免语法歧义,作为参数传递的负数必须用圆括号包裹:
ghci> 10 + (-3)
7
ghci> 10 + -3 -- 语法错误:编译器无法解析两个连续的中缀操作符
在 Haskell 中,操作符与普通函数之间可以双向转换:
中缀转前缀:用圆括号包裹中缀运算符,即可当作普通前缀函数调用:
ghci> (+) 10 20
30
ghci> (*) 5 6
30
前缀转中缀:用反引号包裹双参数函数名(例如 10 `div` 2),即可作为中缀运算符使用:
ghci> 10 `div` 2
5
ghci> 17 `mod` 5
2
用圆括号包裹中缀运算符及其单侧参数,可以得到一个单参数函数:
左侧截断:(2 ^) 等价于 \x -> 2 ^ x (以 2 为底的指数运算)。
右侧截断:(^ 2) 等价于 \x -> x ^ 2 (求平方)。
除法截断:(/ 2) 表示除以 2。
警告
一元减号的截断问题:
表达式 (- 2) 在 Haskell 中并不表示“减去 2 的函数”,而是表示负数 -2 本身。
如果需要表达“减去 2”的函数,可以使用标准库提供的 subtract 2,或显式写成 Lambda:\x -> x - 2。
Haskell 中的函数调用拥有最高的优先级(优先级 10),其结合性为左结合:
f x + g y -- 等价于 (f x) + (g y),不会被解析为 f (x + g) y
每一个中缀操作符都有对应的优先级(0 到 9)和结合性(左结合 infixl、右结合 infixr、无结合 infix)。可以在 GHCi 中使用 :i 查看:
ghci> :i (+)
infixl 6 +
ghci> :i (*)
infixl 7 *
ghci> :i (^)
infixr 8 ^
乘法优先级为 7,高于加法的 6,因此 2 + 3 * 4 会先计算乘法;而乘方 ^ 是优先级为 8 的右结合操作符,因此 2 ^ 3 ^ 2 等价于 2 ^ (3 ^ 2) = 512。
当需要在局部范围定义临时变量或辅助函数时,Haskell 提供了两种语法:let ... in ... 和 where。
let 是一个表达式,本身有返回值,可以嵌套在任何允许表达式出现的位置:
cylinderArea :: Double -> Double -> Double
cylinderArea r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^ 2
in sideArea + 2 * topArea
where 是声明级语法,紧跟在函数定义的方程右侧,用于为整个函数(包括其守卫分支)提供辅助定义:
cylinderArea' :: Double -> Double -> Double
cylinderArea' r h = sideArea + 2 * topArea
where
sideArea = 2 * pi * r * h
topArea = pi * r ^ 2
二者的选择:
let 适合小范围临时值计算,或嵌套在列表推导式、单子代码块中。
where 把主计算逻辑放在上面,辅助定义放在下面,适合自顶向下阅读。
字符(Char):单引号包裹的单个 Unicode 字符,如 'a'、'中'。
字符串(String):双引号包裹,在底层等价于字符的链表 [Char]。
这里只列出最基本的操作,字符处理、拆分拼接、与数字互转以及 Text 类型会在字符串一章单独介绍。
常用基础列表操作:
列表拼接:++
头部添加元素(Cons)::
获取首元素:head (空列表上会抛异常)
获取尾部列表:tail (空列表上会抛异常)
截取前 N 个元素:take
舍弃前 N 个元素:drop
列表长度:length
ghci> 'H' : "ello"
"Hello"
ghci> "Hello, " ++ "World!"
"Hello, World!"
ghci> take 2 "Haskell"
"Ha"
ghci> drop 2 "Haskell"
"skell"
在空列表上调用 head [] 或 tail [] 会抛出运行时异常:
ghci> head []
*** Exception: Prelude.head: empty list
实际代码中应尽量避免直接调用 head 与 tail,优先用模式匹配,或使用来自 Data.List 的安全解构函数 uncons:
import Data.List (uncons)
-- uncons :: [a] -> Maybe (a, [a])
safeHead :: [a] -> Maybe a
safeHead xs = case uncons xs of
Nothing -> Nothing
Just (h, _) -> Just h
Haskell 用空格缩进(Offside Rule)划定声明块边界,不要使用 Tab。
中缀操作符可用 (op) 转为前缀,前缀函数可用反引号转为中缀。
操作符截断支持单侧填参,但“减去 n”要写成 subtract n,因为 (- n) 是负数。
函数应用优先级最高(10),操作符的优先级与结合性决定求值次序。
尽量不用 head/tail 这类偏函数,优先用模式匹配。