一看就懂的 Haskell 教程 - Haskell 函数定义与调用机制

15 阅读10分钟

Haskell 作为纯函数式语言,函数是“第一类公民”,其定义和调用机制是整个语言的核心。以下从规范、本质、风格、特殊形式、核心特性五个维度,全方位拆解函数的所有关键规则。

一、函数定义规范

1.1 核心构成:类型签名 + 函数体

Haskell 函数定义的完整形式包含「类型签名(可选但推荐)」和「函数体」,严格遵循“纯函数”原则(无副作用、输入决定输出)。

(1)类型签名规范

  • 语法格式函数名 :: 输入类型1 -> 输入类型2 -> ... -> 返回类型

    • -> 表示函数的“输入→输出”关系,右结合(核心,柯里化的基础);
    • 类型签名是“契约”,编译器会校验函数体是否符合签名;
    • 类型变量(如 a/b)表示多态,需小写开头;具体类型(如 Int/[a])大写开头。
  • 示例

    • -- 单参数函数:计算平方
      square :: Int -> Int  -- 输入Int,返回Int
      square x = x * x
      
      -- 多参数函数:两数相加(数值多态)
      add :: Num a => a -> a -> a  -- 带类型类约束的多态
      add x y = x + y
      
      -- 高阶函数:接收函数+列表,返回列表
      applyTwice :: (a -> a) -> [a] -> [a]  -- 输入:函数(a→a) + 列表[a],返回列表[a]
      applyTwice f xs = map f (map f xs)
      

(2)函数名/参数/返回值规则

  • 函数名

    • 普通函数名:小写开头(如 square/add),可包含字母、数字、'_
    • 运算符(中缀函数):由特殊符号组成(如 +/*/<$>),可通过括号转为前缀调用(如 (+))。
  • 参数

    • 无括号、无逗号分隔,空格分隔即可(如 add x y 而非 add(x,y));
    • 参数是“模式”,支持模式匹配(如 sum [] = 0; sum (x:xs) = x + sum xs)。
  • 返回值

    • 函数体最后一个表达式的值即为返回值,无 return 关键字(return 是 Monad 操作,非返回值);
    • 纯函数必须有返回值(无 void 函数,空值用 () 表示)。

1.2 顶层函数 vs 局部函数

维度顶层函数局部函数
定义位置模块顶层(不在任何函数/表达式内)函数体/do块/where/let 表达式内
作用域整个模块(或导出到其他模块)仅定义所在的局部作用域
类型签名推荐显式写(增强可读性)可省略(编译器自动推导)
常见场景模块对外暴露的核心功能辅助逻辑、临时复用、闭包

示例:局部函数的两种写法(where / let)

-- 顶层函数:计算三角形周长(依赖局部函数)
trianglePerimeter :: Double -> Double -> Double -> Double
trianglePerimeter a b c = sideSum  -- 最后一个表达式是返回值
  where  -- where 绑定:局部函数,作用域是顶层函数体
    sideSum = a + b + c  -- 局部变量(本质是无参函数)
    validate = a > 0 && b > 0 && c > 0  -- 局部辅助函数

-- let 绑定:局部函数,表达式内生效
circleArea :: Double -> Double
circleArea r = 
  let pi' = 3.14159  -- 局部常量
      square x = x * x  -- 局部函数
  in pi' * square r  -- in 后是返回值

二、多参数函数的本质(柯里化)

Haskell 中没有真正的“多参数函数” ,所有多参数函数都是“返回函数的函数”,这是由 -> 的右结合特性决定的。

2.1 柯里化(Currying)核心原理

  • 定义:将接收 N 个参数的函数,转换为 N 个嵌套的单参数函数。

  • 语法体现a -> b -> ca -> (b -> c)(右结合)。

  • 示例拆解

    • -- 表面:两参数函数
      add :: Int -> Int -> Int
      add x y = x + y
      
      -- 本质:单参数函数,返回另一个单参数函数
      add :: Int -> (Int -> Int)
      add x = \y -> x + y  -- 等价写法(λ表达式)
      

2.2 部分应用(Partial Application)

柯里化的核心价值:调用函数时可只传部分参数,返回“等待剩余参数的函数”。

示例:部分应用实战

-- 1. 部分应用 add,固定第一个参数为 5
add5 :: Int -> Int
add5 = add 5  -- 等价于 add5 y = 5 + y

-- 2. 部分应用列表映射函数,固定映射逻辑
incrementAll :: [Int] -> [Int]
incrementAll = map (+1)  -- map 的第一个参数是 (Int→Int),返回 ([Int]→[Int])

-- 3. 多参数运算符的部分应用(sections 语法,下文详解)
isPositive :: Int -> Bool
isPositive = (> 0)  -- 部分应用 >,固定右侧参数为 0

2.3 函数作为一等公民

Haskell 中函数和整数/字符串一样,可:

  1. 作为参数传递(高阶函数);
  2. 作为返回值返回(闭包);
  3. 绑定到变量;
  4. 存入数据结构(如列表、元组)。

示例:函数作为一等公民

-- 1. 函数作为参数(高阶函数)
apply :: (a -> b) -> a -> b
apply f x = f x

-- 2. 函数作为返回值(闭包:捕获外部变量)
makeAdder :: Int -> (Int -> Int)
makeAdder n = \x -> x + n  -- 捕获 n,返回的函数可使用 n

-- 3. 函数存入列表
mathOps :: [Int -> Int -> Int]
mathOps = [(+), (-), (*)]  -- 列表元素是函数

-- 4. 函数绑定到变量
multiply = (*)  -- multiply 是函数变量

三、函数调用风格

3.1 前缀调用 vs 中缀调用

调用风格语法格式适用场景示例
前缀调用函数名 参数1 参数2 ...普通函数、多参数函数add 1 2map (+1) [1,2,3]
中缀调用参数1 函数名 参数2运算符、双参数函数1 + 2[1,2] ++ [3,4]

关键规则:

  • 普通函数可通过 `函数名` 转为中缀调用;
  • 运算符可通过 (运算符) 转为前缀调用。

示例:两种风格互转

-- 普通函数转中缀
1 `add` 2  -- 等价于 add 1 23

-- 运算符转前缀
(+) 1 2    -- 等价于 1 + 23
(++) [1] [2]  -- 等价于 [1] ++ [2][1,2]

3.2 运算符优先级与结合性

Haskell 定义了运算符的优先级(0-9,越高越先执行)和结合性(左/右/无),避免歧义。

核心规则:

  1. 优先级:数字越大,优先级越高(如 * 优先级 7,+ 优先级 6,故 1 + 2 * 3 = 7);

  2. 结合性

    1. 左结合(如 +/*):a + b + c = (a + b) + c
    2. 右结合(如 ->/:):a : b : c = a : (b : c)
    3. 无结合(如 ==/<):不能连续调用(需加括号,如 (a == b) && (b == c))。

自定义运算符:

可通过 infixl/infixr/infix 定义自定义运算符的优先级和结合性:

-- 定义自定义运算符 +++:优先级 6,左结合
infixl 6 +++
(+++) :: [a] -> [a] -> [a]
xs +++ ys = xs ++ ys  -- 等价于 ++

-- 调用:优先级与 + 相同,左结合
[1,2] +++ [3,4] +++ [5,6]  -- 等价于 ([1,2] +++ [3,4]) +++ [5,6]

3.3 Sections

Sections 是运算符部分应用的简化语法,分为“左 section”和“右 section”,核心是固定运算符的一侧参数。

Section 类型语法等价写法示例
右 section(运算符 常量)\x -> x 运算符 常量(> 0)\x -> x > 0
左 section(常量 运算符)\x -> 常量 运算符 x(1 +)\x -> 1 + x

实战场景:

-- 1. 过滤正数(右 sectionfilter (> 0) [-1, 2, -3, 4]  -- → [2,4]

-- 2. 所有元素加 10(左 section)
map (10 +) [1,2,3]  -- → [11,12,13]

-- 3. 列表拼接固定前缀(左 section)
map ("prefix: " ++) ["a", "b"]  -- → ["prefix: a", "prefix: b"]

-- 注意:无参数的运算符(如 negation -)需特殊处理
map (-) [1,2,3]  -- 错误:- 是单目运算符,需写成 map (0 -) [1,2,3][-1,-2,-3]

四、匿名函数(λ表达式)

匿名函数是“无需命名的临时函数”,核心用于:临时逻辑、高阶函数参数、局部简短逻辑。

4.1 基本语法

  • 格式\参数1 参数2 ... -> 表达式

    • `` 是 λ 的简写(λ 在 Haskell 中需转义);
    • 参数空格分隔,-> 后是函数体(单个表达式);
    • 函数体可嵌套、可调用其他函数。

4.2 单/多参数匿名函数

-- 1. 单参数:计算平方
\x -> x * x  -- 等价于 square x = x * x

-- 2. 多参数:两数相加
\x y -> x + y  -- 等价于 add x y = x + y

-- 3. 高阶匿名函数:接收函数+值,调用两次
\f x -> f (f x)  -- 等价于 applyTwice f x = f (f x)

4.3 嵌套匿名函数

匿名函数可嵌套,用于复杂逻辑的临时封装:

-- 嵌套匿名函数:先加 1,再平方
map (\x -> (\y -> y * y) (x + 1)) [1,2,3]  -- → [4,9,16]

-- 简化写法(去掉内层命名)
map (\x -> (x + 1) * (x + 1)) [1,2,3]

4.4 实战场景(匿名函数的核心用途)

场景1:高阶函数的临时参数(最常用)

-- 排序:按元组第二个元素降序(临时逻辑,无需命名)
sortBy ((a,b) (c,d) -> compare d b) [(1,3), (2,1), (3,2)]
-- → [(1,3), (3,2), (2,1)]

-- 过滤:字符串长度大于 5
filter (\s -> length s > 5) ["a", "hello", "haskell"]
-- → ["haskell"]

场景2:部分应用的补充(无法用 sections 时)

-- 需求:列表元素乘以 2 再加 3(无法用 sections 简化)
map (\x -> x * 2 + 3) [1,2,3]  -- → [5,7,9]

场景3:闭包(捕获外部变量)

-- 动态生成匿名函数:捕获外部变量 n
makeScaler :: Int -> (Int -> Int)
makeScaler n = \x -> x * n  -- 捕获 n,返回匿名函数

scaleBy5 = makeScaler 5
scaleBy5 10  -- → 50

4.5 匿名函数的限制

  • 函数体只能是单个表达式(不能有多行、多个语句);
  • 复杂逻辑建议提取为命名函数(where/let),提升可读性。

五、函数赋值与传参(核心特性)

5.1 函数变量绑定

Haskell 中函数可绑定到变量,变量本质是“函数的别名”,无“赋值”概念(不可变)。

-- 1. 简单绑定:add 绑定到 (+)
add = (+)  -- add 是 (+) 的别名,类型:Num a => a -> a -> a

-- 2. 部分应用绑定:add10 是 add 固定第一个参数的结果
add10 = add 10  -- 类型:Num a => a -> a

-- 3. 多态函数绑定
identity :: a -> a
identity = \x -> x  -- 绑定匿名函数到 identity

5.2 函数作为参数(高阶函数)

这是函数式编程的核心,Haskell 内置大量高阶函数(map/filter/foldl/foldr)。

示例:自定义高阶函数

-- 1. 通用遍历函数:对列表每个元素应用函数
forEach :: (a -> ()) -> [a] -> ()
forEach f [] = ()
forEach f (x:xs) = f x >> forEach f xs  -- IO 上下文示例

-- 调用:打印列表每个元素
forEach (\x -> putStrLn ("Element: " ++ show x)) [1,2,3]

-- 2. 函数组合器:将两个函数组合为一个(f . g = \x -> f (g x))
compose :: (b -> c) -> (a -> b) -> a -> c
compose f g = \x -> f (g x)

-- 调用:先平方,再加 1
add1AfterSquare = compose (+1) (\x -> x * x)
add1AfterSquare 3  -- → 10

5.3 函数作为返回值(闭包)

函数可返回另一个函数,且返回的函数可“捕获”外部作用域的变量(闭包)。

示例:闭包实战

-- 1. 计数器(纯函数版:无状态,每次返回新函数)
counter :: Int -> (Int, Int -> Int)
counter n = (n, \x -> counter (n + x) |> fst)  -- 简化写法

-- 调用:
let (c1, inc) = counter 0
c1  -- → 0
let c2 = inc 1
c2  -- → 1
let c3 = inc 2
c3  -- → 3

-- 2. 配置化函数:返回定制化的验证函数
makeValidator :: String -> (String -> Bool)
makeValidator prefix = \s -> prefix `isPrefixOf` s  -- 捕获 prefix

-- 调用:
validateUser = makeValidator "user_"
validateUser "user_123"  -- → True
validateUser "admin_456"  -- → False

5.4 函数传参的特殊规则

  1. 惰性求值:参数仅在需要时计算(非严格求值);

  2. 模式匹配传参:参数可直接写模式,编译器自动校验穷尽性;

    1. -- 模式匹配传参:处理 Maybe 类型
      handleMaybe :: Maybe a -> String
      handleMaybe Nothing = "Empty"
      handleMaybe (Just x) = "Value: " ++ show x
      
  3. 无副作用传参:参数是纯值,多次传相同参数结果一致(引用透明)。

总结(核心关键点)

  1. 函数定义:类型签名(推荐显式)+ 函数体,纯函数无副作用,参数支持模式匹配;
  2. 多参数本质:柯里化(嵌套单参数函数),部分应用是核心优势;
  3. 调用风格:前缀/中缀可互转,sections 简化运算符部分应用,优先级/结合性避免歧义;
  4. 匿名函数:λ表达式,用于临时逻辑,函数体仅单个表达式;
  5. 一等公民:函数可作为参数/返回值/变量,闭包可捕获外部变量,是高阶函数的基础。