基础语法与表达式

Haskell 的语法风格与传统的 C 族语言差别很大。程序由表达式构成,没有命令式语言中的“变量赋值”与“语句顺序执行”概念。

代码注释与排版缩进规则

单行与多行注释

-- 这是单行注释

{-
  这是多行块级注释。
  {- 多行注释支持嵌套 -}
-}

越界缩进规则(Layout / Offside Rule)

与 Python 类似,Haskell 使用缩进而非大括号与分号来确定代码块的边界。这套机制在语言规范中被称为越界规则(Offside Rule):

  1. 同级对齐原则:处于同一个逻辑块(如 where、let 或 do)内的多个声明,必须保持相同的起始列缩进。

  2. 续行缩进原则:如果一个长表达式拆分为多行,后续行必须比起始声明行缩进更深。

  3. 不要使用 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    -- 语法错误:编译器无法解析两个连续的中缀操作符

中缀运算符、前缀函数与截断(Sectioning)

中缀与前缀转换

在 Haskell 中,操作符与普通函数之间可以双向转换:

  1. 中缀转前缀:用圆括号包裹中缀运算符,即可当作普通前缀函数调用:

    ghci> (+) 10 20
    30
    ghci> (*) 5 6
    30
    
  2. 前缀转中缀:用反引号包裹双参数函数名(例如 10 `div` 2),即可作为中缀运算符使用:

    ghci> 10 `div` 2
    5
    ghci> 17 `mod` 5
    2
    

操作符截断(Operator Sectioning)

用圆括号包裹中缀运算符及其单侧参数,可以得到一个单参数函数:

  • 左侧截断:(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。

局部作用域:let 与 where

当需要在局部范围定义临时变量或辅助函数时,Haskell 提供了两种语法:let ... in ... 和 where。

let 表达式

let 是一个表达式,本身有返回值,可以嵌套在任何允许表达式出现的位置:

cylinderArea :: Double -> Double -> Double
cylinderArea r h =
  let sideArea = 2 * pi * r * h
      topArea  = pi * r ^ 2
  in  sideArea + 2 * topArea

where 子句

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 这类偏函数,优先用模式匹配。