纯函数:函数式编程的“原子”
函数式编程的第一原则,是纯函数——它像数学里的函数一样“诚实”:输入确定,输出就确定,没有任何隐藏的副作用(比如修改全局变量、打印日志、操作文件)。

举个Haskell的例子:
-- 纯函数:计算一个数的平方
square :: Int -> Int
square x = x * x -- 输入5,一定输出25;输入10,一定输出100
再看Scala的纯函数:
// 纯函数:计算两个数的和
def add(a: Int, b: Int): Int = a + b -- 不管调用多少次,1+2永远是3
反过来,非纯函数就像“会偷偷搞事情”的函数。比如Haskell里依赖全局变量的函数:
-- 非纯函数:依赖全局计数器
counter :: Int
counter = 0
increment :: Int -> Int
increment x = x + counter -- 若counter被修改,结果会变!比如counter变成1,输入5会输出6
Scala里修改外部变量的函数更直观:
var globalCount = 0 -- 可变的全局变量
def impureIncrement(a: Int): Int = {
globalCount += a -- 修改了外部状态,这就是副作用
globalCount
}
为什么纯函数重要?因为它可测试(不用 mock 外部状态)、可复用(哪里需要哪里搬)、可并行(没有竞态条件)。你有没有遇到过“改了一行代码,整个系统崩了”的情况?大概率是因为用了非纯函数——纯函数能帮你把这种“意外”降到最低。
不可变数据:告别“意外修改”的安全感
函数式编程里还有个“反直觉”的规则:数据一旦创建,就不能修改。Haskell从语言层面强制不可变,Scala则给了你选择(用val
定义不可变变量,var
定义可变变量,但函数式风格里尽量不用var
)。
先看Haskell的不可变列表:
originalList :: [Int]
originalList = [1,2,3] -- 创建后永远是[1,2,3]
newList :: [Int]
newList = 0 : originalList -- 生成新列表[0,1,2,3],originalList没变化
Haskell里没有“修改列表某个元素”的操作,所有“修改”都是生成新数据。
Scala里的不可变数据更灵活,比如case class
默认不可变:
// 不可变的用户类型
case class User(name: String, age: Int)
val alice = User("Alice", 25) -- 用val定义,alice的name和age永远不能改
// alice.age = 26 -- 编译错误!想改?生成新对象:
val olderAlice = alice.copy(age = 26) -- 新对象,旧对象不变
不可变数据的好处是什么?线程安全——多线程环境下不用加锁,因为没人能修改数据;可追溯——数据变化的每一步都有记录(比如originalList
→newList
),调试时能快速定位问题。你有没有遇到过“不知道谁改了我的变量”的崩溃?不可变数据能直接解决这个痛点。
高阶函数:让函数变“积木”
函数式编程里,函数是“一等公民”——它能像变量一样被传递、被返回、被作为参数。而接受函数作为参数,或返回函数的函数,就是高阶函数。
最常见的高阶函数是map
(遍历数据并转换)和filter
(筛选数据)。先看Haskell的map
:
-- 用map把列表里的每个元素乘以2
doubleList :: [Int] -> [Int]
doubleList = map (*2) -- (*2)是一个“乘以2”的函数,作为map的参数
-- 调用:doubleList [1,2,3] → [2,4,6]
Scala的map
用法几乎一样,但更贴近OOP风格:
// Scala的List是不可变的,map返回新列表
val numbers = List(1,2,3)
val doubled = numbers.map(_ * 2) -- _*2是匿名函数,等价于x => x*2,结果[2,4,6]
再看更复杂的高阶函数——Haskell的foldl
(折叠列表计算总和):
-- 用foldl计算列表的和
sumList :: [Int] -> Int
sumList = foldl (+) 0 -- (+)是加法函数,0是初始值
-- 调用:sumList [1,2,3] → 6
Scala里对应的是foldLeft
:
val sum = numbers.foldLeft(0)(_ + _) -- 初始值0,累加每个元素,结果6
高阶函数的核心价值是抽象重复逻辑。比如你要写“把列表里的每个元素转成字符串”“把列表里的每个元素加1”——不用写两个循环,用map
就能搞定。想象一下:函数变成了“积木”,你可以用不同的“积木”(函数)组合出不同的功能,这比写一堆重复代码爽多了!
延迟计算:按需执行的效率魔法
延迟计算(也叫惰性求值)是函数式编程的“效率神器”——它不会立即计算结果,而是等到真正需要的时候才执行。Haskell默认是延迟计算,Scala则用LazyList
实现(之前叫Stream
)。
先看Haskell的无限列表:
-- 生成从1开始的无限列表
infiniteList :: [Int]
infiniteList = [1..] -- 理论上无限,但不会立即生成所有元素
-- 取前5个元素,此时才会计算
take5 :: [Int]
take5 = take 5 infiniteList -- 结果[1,2,3,4,5]
如果是 imperative 语言(比如Java),生成无限列表会直接内存溢出,但Haskell因为延迟计算,只计算需要的部分。
Scala的LazyList
用法类似:
// 生成无限序列
val lazyInfinite = LazyList.from(1) -- 不会立即生成元素
val take5Scala = lazyInfinite.take(5).toList -- 按需计算前5个,结果List(1,2,3,4,5)
延迟计算的好处是什么?处理大数据或无限数据时省内存。比如你要读取一个10GB的日志文件,不用一次性加载到内存,用延迟计算可以“读一行处理一行”;再比如生成斐波那契数列,不用预先计算所有项,需要多少取多少。
模式匹配:比switch更聪明的分支处理
函数式编程里的模式匹配,是比switch
/if-else
更强大的分支处理工具——它能匹配数据的结构(比如列表的头和尾、case class
的字段),还能自动检查“是否覆盖所有情况”。
先看Haskell的列表模式匹配:
-- 计算列表的长度(递归实现)
listLength :: [a] -> Int
listLength list = case list of
[] -> 0 -- 空列表,长度0
x:xs -> 1 + listLength xs -- 非空列表,取头x,尾xs,长度+1
这里的x:xs
是Haskell的列表模式——x
是列表的第一个元素,xs
是剩下的元素。
Scala的模式匹配更灵活,能匹配case class
:
// 定义一个表示形状的case class
sealed trait Shape -- 密封特质,所有子类必须在同一个文件里
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
// 用模式匹配计算面积
def calculateArea(shape: Shape): Double = shape match {
case Circle(r) => math.Pi * r * r -- 匹配Circle,取radius为r
case Rectangle(w, h) => w * h -- 匹配Rectangle,取width和height
case Triangle(b, h) => 0.5 * b * h -- 匹配Triangle,取base和height
}
模式匹配的优势在哪里?更简洁(不用写一堆getter
)、更安全(密封特质会检查是否覆盖所有子类,避免遗漏)。比如Scala里如果漏了Triangle
的情况,编译时会报错——这比switch
的“漏了case也不提醒”贴心多了!
从概念到实践:为什么要学函数式编程?
看到这里,你可能会问:“这些概念听起来不错,但实际开发中有用吗?”
比如在Scala中,Spark(大数据框架)的核心API就是函数式风格——map
、filter
、reduce
这些高阶函数,正是处理大数据的基础;Haskell则在编译器、金融系统(需要高精度计算)中广泛应用,因为纯函数和不可变数据能保证计算的正确性。
更重要的是,函数式编程能改变你的思维方式——从“怎么修改数据”变成“怎么组合函数”,从“解决具体问题”变成“抽象通用逻辑”。这种思维升级,会让你写出更简洁、更可靠、更易维护的代码。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/333