函数式编程
1. 编程范式
我们要讲函数式编程,那肯定说明除了函数式编程还有其他编程,是的,还有命令式编程、声明式编程、面向对象编程等这些统称为编程范式,啥叫编程范式,说白了就是一种编程的方法论
编程范式是一种基本的编程风格或方法,遵循一系列特定的概念、原则和实践。它塑造了程序员使用编程语言解决问题的方式。不同的范式代表着不同的思考和构建代码的方式,它们通常强调编程的不同方面。
2. 生活举例
2.1. 面向过程编程
面向过程,就是按照我们分析好了的步骤,按部就班的依次执行就行了
1、打开洗衣机
2、放入衣服
3、放入洗衣液
4、关上洗衣机
2.2. 面向对象编程
所谓的面向对象,就是在编程的时候尽可能的去模拟真实的现实世界,按照现实世界中的逻辑去处理一个问题,分析问题中参与其中的有哪些实体,这些实体应该有什么属性和方法,我们如何通过调用这些实体的属性和方法去解决问题。
1、洗衣机.打开洗衣机
2、人.放衣服
3、人.放洗衣液
4、洗衣机.关上洗衣机
2.3. 函数式编程
函数式编程,大量使用函数,减少代码重复,提升开发效率;接近自然语言,易于理解;因为不依赖外界状态,只要给定输入参数,结果必定相同,方便代码管理;因为不存在修改变量,天生更易于并发,也能理解,GO语言默认是传值的。
3. 代码示例
假设我们有一个整数列表[1, 2, 3, 4, 5, 6],我们希望计算其中所有偶数的平方之和
3.1. 面向对象编程
func main() {numbers := []int{1, 2, 3, 4, 5, 6}result := 0for _, num := range numbers {if num%2 == 0 {result += num * num}}fmt.Println(result)
}
3.2. 面向过程编程
type NumberList struct {numbers []int
}func (n *NumberList) SumEvenSquares() int {sum := 0for _, num := range n.numbers {if num%2 == 0 {sum += num * num}}return sum
}func main() {numbers := &NumberList{numbers: []int{1, 2, 3, 4, 5, 6}}sum := numbers.SumEvenSquares()fmt.Println(sum)
}
3.3. 函数式编程
func isEven(n int) bool {return n%2 == 0
}func square(n int) int {return n * n
}func processList(numbers []int, filter func(int) bool, mapper func(int) int) int {var sum intfor _, num := range numbers {if filter(num) {sum += mapper(num)}}return sum
}func main() {numbers := []int{1, 2, 3, 4, 5, 6}sum := processList(numbers, isEven, square)fmt.Printf("The sum of squares of even numbers is %d\n", sum)
}
编程范式 | 示例代码 | 特点 | 适用场景 |
---|---|---|---|
面向过程编程 | for 循环和条件判断 | 简单直观,数据和操作分离 | 小型项目和简单任务 |
面向对象编程 | 类和方法封装 | 封装性好,复用性强 | 大型项目和复杂系统 |
函数式编程 | 纯函数和高阶函数 | 函数组合,避免副作用 | 并发编程和高复用性场景 |
3.4. Go 语言的函数是一等公民
3.4.1. 一等公民
在编程语言中,“一等公民”(First-Class Citizen)指的是某个语言元素(如函数、变量、对象等)可以像其他基本数据类型一样被操作。在Go语言中,函数被设计为一等公民,这意味着它们可以在程序中被灵活地使用,具有很高的灵活性和强大的功能。
- 存储在变量中:函数可以像整数或字符串一样,存储到变量中。
- 作为参数传递:函数可以作为参数传递给其他函数,从而实现高阶函数。
- 作为返回值返回:函数可以从其他函数中返回,这为动态生成和组合函数提供了可能。
- 具有一等类型:函数在Go语言中具有一等类型,可以像其他类型(如整数、字符串等)一样被声明和使用。
3.4.2. 与函数式编程的关系
函数式编程是一种编程范式,强调使用纯函数和不可变数据,通过函数的组合和抽象来构建程序。Go语言中函数一等公民的特性使得它能够支持函数式编程的许多核心思想。
高阶函数:由于函数可以作为参数传递,Go语言可以轻松实现高阶函数,即接受函数作为参数或返回函数的函数。例如,Go中的sort
包中的Sort
函数,可以接受一个自定义的比较函数作为参数,从而实现灵活的排序。
闭包(Closure):闭包是指一个函数值,它引用了其词法作用域中的变量。Go语言支持闭包,这使得函数可以捕获和保留其所在作用域的变量,从而实现更复杂的功能,如状态维护和延迟计算。
函数组合:函数式编程强调通过组合简单的函数来构建复杂的程序。Go语言的函数一等公民特性使得函数可以像积木一样被组合在一起,实现更强大的功能。
3.4.3. 与其他语言的区别
- C语言:
- C语言中的函数指针也具有一些类似一等公民的特性,但它们的功能相对有限,不能像Go中的函数一样灵活。C语言的函数指针不能存储在变量中,也不能作为参数传递给函数。
- Java语言:
- Java中的函数并不是一等公民。虽然Java 8引入了Lambda表达式,但这只是对函数式编程的一种有限的支持。Java中的Lambda表达式必须绑定到接口的单个抽象方法(SAM)上,因此它们的灵活性不如Go中的函数。
- JavaScript语言:
- JavaScript中的函数也是一等公民,与Go语言类似。但JavaScript是动态类型的,而Go是静态类型的,因此在函数的类型安全和性能方面有所不同。
4. 函数式编程特点
4.1. 只用"表达式",不用"语句"
“表达式”(expression)是一个单纯的运算过程,总是有返回值;“语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
4.2. 没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
4.3. 不修改状态
状态一般是通过变量来保存,不修改状态意味着不能修改输入值,而是每次返回输出值,通过输出保存状态。
4.4. 引用透明
引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
4.5. 高度模块化和组合性
由于函数可以作为一等公民被传递和组合,函数式编程使得代码更加模块化和易于重用。
4.5. 惰性计算
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。
第1~3点是一致的,通过不修改状态、使用返回值,达到没有副作用的效果。
5. 函数式编程核心概念
我们先了解几个函数式编程的重要概念:
- 纯函数 (
Pure Function
): 输出仅依赖输入参数,无副作用。 - 高阶函数 (
Higher-Order Function
): 接受函数作为参数或返回函数。 - 不可变性 (
Immutability
): 数据在创建后不应被修改。 - 函数组合 (
Function Composition
): 将多个小函数组合成复杂的功能。 - 惰性求值 (
Lazy Evaluation
): 按需计算而非立即求值。
5. 1. 纯函数
在 Go
中,纯函数很容易实现。以下是一个简单的例子:
func add(a, b int) int {return a + b
}
add
是一个纯函数,因为它的输出完全由输入 a
和 b
决定,且没有副作用。
更复杂的场景中,遵循纯函数原则可以避免意外的状态修改。例如:
func filter(numbers []int, predicate func(int) bool) []int {var result []intfor _, n := range numbers {if predicate(n) {result = append(result, n)}}return result
}
filter
是一个纯函数,传入相同的参数会返回相同的结果。
5.2. 高阶函数
将函数整合起来 - 高阶函数(Higher-order Functions)
高阶函数的定义。满足以下其中一个条件即可称为高阶函数:
- 接受一个或者多个函数作为其入参
- 返回值是一个函数
如果一个函数的参数和返回值都是或者其中一个是另外一个函数,那这个函数就是高阶函数
高阶函数像骨架一样支起程序的整体结构,具体的实现则由作为参数传入的具体函数来实现。因此,我们看到高阶函数提供了一种能力,可以将普通函数(功能模块)整合起来,使得任一普通函数都能被灵活的替换和复用。
5.3.不可变性
Go
中的切片和映射本质上是可变的,但我们可以通过创建新值来模拟不可变性。例如:
func appendImmutable(slice []int, value int) []int {newSlice := make([]int, len(slice)+1)copy(newSlice, slice)newSlice[len(slice)] = valuereturn newSlice
}func main() {original := []int{1, 2, 3}newSlice := appendImmutable(original, 4)fmt.Println(original) // 输出: [1 2 3]fmt.Println(newSlice) // 输出: [1 2 3 4]
}
通过这种方式,我们可以避免意外的状态修改,确保数据安全性。
5.4.函数组合
函数组合是一种将多个简单函数组合成复杂功能的技巧。在 Go
中,我们可以借助高阶函数实现:
func compose(f, g func(int) int) func(int) int {return func(x int) int {return f(g(x))}}func double(n int) int {return n * 2}func increment(n int) int {return n + 1}func main() {doubleThenIncrement := compose(increment, double)fmt.Println(doubleThenIncrement(3)) // 输出: 7}
compose
函数实现了两个函数的组合,让代码更具模块化。
5.5. 惰性计算
Go
不直接支持惰性求值,但我们可以通过管道 (channe
l) 和生成器模拟:
func generator(nums ...int) <-chan int {out := make(chan int)go func() {for _, n := range nums {out <- n}close(out)}()return out
}func mapChan(in <-chan int, f func(int) int) <-chan int {out := make(chan int)go func() {for v := range in {out <- f(v)}close(out)}()return out
}func main() {nums := generator(1, 2, 3, 4)squares := mapChan(nums, func(n int) int { return n * n })for v := range squares {fmt.Println(v) // 输出: 1 4 9 16}
}
通过使用 channel
,我们可以构建惰性计算流水线,只在需要时才进行求值。
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。一个惰性计算的例子是生成无穷 Fibonacci 列表的函数,但是对第n个Fibonacci 数的计算相当于只是从可能的无穷列表中提取一项。
6.源码中的函数式编程案例
strings.map()
7.函数式编程的优缺点
优点:
变量不可变,引用透明,天生适合并发。表达方式更加符合人类日常生活中的语法,代码可读性更强。实现同样的功能函数式编程所需要的代码比面向对象编程要少很多,代码更加简洁明晰。函数式编程广泛运用于科学研究中,因为在科研中对于代码的工程化要求比较低,写起来更加简单,所以使用函数式编程开发的速度比用面向对象要高很多,如果是对开发速度要求较高但是对运行资源要求较低同时对速度要求较低的场景下使用函数式会更加高效。
缺点:
由于所有的数据都是不可变的,所以所有的变量在程序运行期间都是一直存在的,非常占用运行资源。同时由于函数式的先天性设计导致性能一直不够。虽然现代的函数式编程语言使用了很多技巧比如惰性计算等来优化运行速度,但是始终无法与面向对象的程序相比,当然面向对象程序的速度也不够快。函数式编程虽然已经诞生了很多年,但是至今为止在工程上想要大规模使用函数式编程仍然有很多待解决的问题,尤其是对于规模比较大的工程而言。如果对函数式编程的理解不够深刻就会导致跟面相对象一样晦涩难懂的局面。