Swift编程思想(二) - 面向函数式编程

Swift 的函数式编程是一种编程范式,它强调使用函数来处理数据和表达程序逻辑。
Swift 的函数式编程核心思想是使用一等函数(first-class functions)和不可变性(immutability)来编写代码,这样可以更容易地推理和测试。
函数式编程鼓励使用纯函数(输入完全决定输出,无副作用)和高阶函数(可以接受其他函数作为参数或返回函数的函数)。

 let numbers = [1, 2, 3, 4, 5]
 ​
 // 使用 map 函数将每个数字乘以 2
 let doubledNumbers = numbers.map { $0 * 2 }
 ​
 // 使用 filter 函数筛选出偶数
 let evenNumbers = numbers.filter { $0 % 2 == 0 }
 ​
 // 使用 reduce 函数计算所有数字的总和
 let sumOfNumbers = numbers.reduce(0, +)
 ​
 print(doubledNumbers) // 输出 [2, 4, 6, 8, 10]
 print(evenNumbers)     // 输出 [2, 4]
 print(sumOfNumbers)   // 输出 15

在这个例子中,map, filter, 和 reduce 都是以不可变的方式工作的,每个都返回一个新的数组,原始数组 numbers 保持不变。这使得函数式编程在 Swift 中非常有力且易于理解。

Swift 适合函数式编程的原因

因为它在设计上融入了许多函数式编程的特性和原则。以下是一些关键原因:

  1. 一等函数(First-Class Functions) :在 Swift 中,函数是“一等公民”。这意味着你可以将函数赋给变量、作为参数传递给其他函数,或者作为其他函数的返回结果。
  2. 不可变性(Immutability) :Swift 鼓励使用常量(let)而不是变量(var),这有助于创建不可变的数据结构,减少副作用,提高代码安全性。
  3. 高阶函数(Higher-Order Functions) :Swift 提供了 mapfilterreduce 等高阶函数,允许你以简洁、声明式的方式处理集合。
  4. 闭包(Closures) :Swift 中的闭包是无名的闭包函数,可以捕获和存储其所在上下文中的任何常量和变量的引用。这一点对于创建函数式接口非常有用。
  5. 可选链(Optional Chaining) :Swift 的可选链语法允许以非常简洁的方式处理可选类型,这与函数式编程中处理可能为空(null)的值的方式相呼应。
  6. 模式匹配(Pattern Matching) :通过 switch 语句和 guard 语句,Swift 支持高级模式匹配,这在函数式编程中经常使用。
  7. 类型推断(Type Inference) :Swift 强大的类型推断使得代码更加简洁,减少了样板代码的需要,这符合函数式编程的精神。
  8. 值类型(Value Types) :Swift 中的结构体(Struct)和枚举(Enum)是值类型,当它们被传递时,其值会被拷贝,这有助于避免共享状态和副作用,是函数式编程常见的实践。
  9. 尾递归优化(Tail Call Optimization) :Swift 编译器对尾递归进行优化,这使得在 Swift 中编写递归函数更为高效,这是函数式编程中常用的一种技术。

因此,Swift 的这些特性和设计理念使其成为实现函数式编程概念的理想选择,既保留了传统命令式编程的优势,又引入了函数式编程的表达力和安全性。

函数式编程的特点

1.一等函数

函数在 Swift 中被当作一等公民,意味着它们可以被赋给变量,可以作为参数传递给其他函数,也可以作为其他函数的返回值。

 func add(_ a: Int, _ b: Int) -> Int {
    return a + b
 }
 ​
 func subtract(_ a: Int, _ b: Int) -> Int {
    return a - b
 }
 ​
 // 将函数作为变量
 let operation: (Int, Int) -> Int = add
 print(operation(3, 2)) // 输出 5
 ​
 // 将函数作为另一个函数的参数
 func applyOperation(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
 }
 ​
 print(applyOperation(5, 3, operation: subtract)) // 输出 2

2.不可变性

函数式编程鼓励使用不可变数据。这意味着你创建的数据结构(如数组、字典等)在创建后不应被修改。这有助于减少副作用和状态改变的相关问题。通常通过使用常量(let)。

 let numbers = [1, 2, 3, 4, 5]
 // 尽管我们对数组进行处理,但原始数组保持不变
 let squaredNumbers = numbers.map { $0 * $0 }
 print(squaredNumbers) // 输出 [1, 4, 9, 16, 25]

3.纯函数

纯函数是其输出值仅由其输入值决定且不产生副作用(如修改全局变量、进行输入/输出操作等)的函数。在 Swift 中,纯函数有助于提高代码的可测试性和可预测性。

 func multiply(_ a: Int, _ b: Int) -> Int {
    return a * b
 }
 // 无论调用多少次,相同的输入总是得到相同的输出
 print(multiply(2, 3)) // 输出 6

4. 高阶函数

Swift 提供了高阶函数的支持,比如 mapfilterreduce 等。这些函数可以接受其他函数作为参数,或者将函数作为返回值。

 let numbers = [1, 2, 3, 4, 5]
 ​
 let doubled = numbers.map { $0 * 2 }
 let even = numbers.filter { $0 % 2 == 0 }
 let sum = numbers.reduce(0, +)
 ​
 print(doubled) // 输出 [2, 4, 6, 8, 10]
 print(even)   // 输出 [2, 4]
 print(sum)     // 输出 15

5. 闭包

Swift 的闭包是可以捕获和存储其上下文中的常量和变量的代码块。闭包特别适合创建快速的回调和自定义操作,是函数式编程的重要组成部分。

 let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
 ​
 let reversed = names.sorted { $0 > $1 }
 print(reversed) // 输出 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

6. 链式调用

Swift 允许你将多个函数调用链接在一起,这样可以写出更简洁和表达力强的代码。

let scores = [60, 85, 95, 70, 80]
let formattedScores = scores
    .filter {
        $0 > 75
    }
    .map {
        "Score: \($0)"
    }
    .joined(separator: ", ")
print(formattedScores)
// 输出 "Score: 85, Score: 95, Score: 80"

7. 强类型系统

Swift 的强类型系统与函数式编程之间的协作主要体现在以下几个方面:

类型安全和清晰的 API 设计

Swift 的强类型系统要求你在编写代码时明确指定变量和函数的类型。这增加了代码的清晰度和可维护性。在函数式编程中,这意味着你可以很清楚地了解每个函数接受什么类型的参数,以及它返回什么类型的结果。

func square(of number: Int) -> Int {
return number * number
}

let squaredNumber = square(of: 5)
// 明确知道返回值是 Int 类型

错误预防

由于类型的严格性,很多可能的错误(如类型不匹配)会在编译时被捕捉,而不是在运行时。这对于函数式编程特别重要,因为函数式编程强调无副作用和不可变性,任何类型错误都可能导致意料之外的副作用。

let result: String = square(of: 5)
// 编译错误,因为 square 返回的是 Int 而不是 String

泛型

Swift 的强类型系统支持泛型编程。泛型让你可以编写灵活、可重用的函数,这些函数可以工作于任何兼容的类型。这是函数式编程中常见的模式,例如在处理集合类型时。

func swapValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}

var number1 = 100
var number2 = 200
swapValues(&number1, &number2) // 正确,类型匹配

var string1 = "Hello"
var string2 = "World"
swapValues(&string1, &string2) // 也正确,泛型函数适用于任何类型

类型推断

Swift 的类型推断减少了声明变量和编写函数时的工作量。尽管它是一个强类型语言,但在很多情况下你不需要明确指定类型。这使得代码更加简洁,同时保持了类型安全。

let numbers = [1, 2, 3, 4, 5] // Swift 推断 numbers 是 [Int] 类型
let doubledNumbers = numbers.map { $0 * 2 } // 推断出 map 的结果是 [Int] 类型

函数类型

在 Swift 中,函数本身也有类型。这意味着你可以像处理其他值一样处理函数,将函数作为参数传递,或者作为返回值,这对于函数式编程风格至关重要。

func add(_ a: Int, _ b: Int) -> Int {
return a + b
}

let function: (Int, Int) -> Int = add
let result = function(2, 3) // 使用函数类型的变量

总结

Swift 的强类型系统为函数式编程提供了结构和安全性,同时其现代化的语言特性,如类型推断和泛型,使得编写函数式代码既灵活又易于理解。这种类型系统与函数式编程理念的结合,使 Swift 成为一个既强大又易于维护代码的编程语言。

8. 惰性求值

Swift 提供了惰性集合,允许你延迟耗时计算,直到真正需要结果时才进行,这在处理大型数据集时特别有用。

 let numbers = [1, 2, 3, 4, 5]
 ​
 let lazySquared = numbers.lazy.map { $0 * $0 }
 print(lazySquared) // 这里仅创建了一个映射,但没有执行任何操作
 ​
 for number in lazySquared {
    print(number) // 真正的计算发生在这里
 }

函数式编程的实际运用

假设你有一个用户列表,每个用户有姓名、年龄和一系列兴趣。你的任务是找出年龄在特定范围内,并且对某个特定兴趣感兴趣的所有用户。

传统方法 vs 函数式方法

在非函数式编程中,你可能会用循环和条件语句来实现这个功能。而在函数式编程中,你可以使用filtermap等高阶函数来实现更简洁、更易读的代码。

代码示例

先定义一个用户模型:

struct User {
    var name: String
    var age: Int
    var interests: [String]
}

然后是用户数据:

let users = [
    User(name: "小明", age: 30, interests: ["篮球", "足球", "排球"]),
    User(name: "大黄", age: 22, interests: ["电影", "编程"]),
    User(name: "小李", age: 35, interests: ["做饭", "睡觉", "篮球"]),
    User(name: "小花", age: 55, interests: ["做饭", "睡觉", "篮球"]),
 ]

现在,使用函数式方法筛选用户:

let selectedInterest = "篮球"
let ageRange = 25...35

let filteredUsers = users.filter { user in
    ageRange.contains(user.age) && user.interests.contains(selectedInterest)
}.map { user in
    user.name
}

// 输出符合条件的用户姓名
print(filteredUsers)

在这个例子中:

  • filter 函数被用来筛选出符合特定年龄范围和兴趣的用户。
  • map函数将筛选出的用户转换成他们的姓名。

这个示例展示了函数式编程的几个优点:

  • 可读性:代码更清晰和简洁,逻辑一目了然。
  • 不可变性:没有修改原始users数组,而是创建了一个新的filteredUsers数组。
  • 链式调用:通过链式调用filtermap,使得代码流畅易懂。

这样的代码更容易理解和维护,也减少了出错的可能性。当然,这只是一个简单的例子。在复杂的应用中,函数式编程的优势会更加明显。