Swift函数式编程

函数式编程简介

函数式编程(Functional Programming):是一种通过应用和组合函数来构造程序的编程范式。它是一种声明性编程范例,其中函数定义是将值映射到其他值的表达式树,而不是更新程序运行状态的命令式语句序列。它将计算视为函数应用的过程,强调使用纯函数(pure functions)进行编程,避免使用可变状态和副作用。函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。函数式编程有以下几个核心概念:

1、纯函数(Pure Functions):

  • 纯函数是函数式编程的基础概念。
  • 纯函数是指具有相同输入始终产生相同输出,并且没有副作用的函数。
  • 纯函数不依赖于外部状态,不修改共享状态,也不进行 I/O 操作。
  • 纯函数的结果仅由输入决定,不受外部环境的影响,因此易于测试、调试和推理。

2、不可变数据(Immutable Data):

  • 函数式编程鼓励使用不可变数据,即数据一旦创建就不能被修改。
  • 不可变数据的不变性确保了数据的可靠性和稳定性,减少了并发访问的竞争条件和错误。
  • 在函数式编程中,不可变数据通过创建新的数据副本来进行修改,而不是在原始数据上进行更改。

3、高阶函数(Higher-Order Functions):

  • 高阶函数是指可以接受一个或多个函数作为参数,并/或返回一个函数作为结果的函数。
  • 高阶函数提供了一种组合和抽象的方式,可以将函数作为数据进行操作和传递。
  • 高阶函数的使用可以更灵活地处理数据和实现通用的功能。

4、函数组合(Function Composition):

  • 函数组合是将多个函数按照特定的顺序连接起来形成一个新的函数的过程。
  • 函数组合可以通过将一个函数的输出作为另一个函数的输入来实现。
  • 函数组合使得代码更加模块化、可读性更高,并且可以通过组合简单的函数来构建复杂的功能。

5、引用透明性(Referential Transparency):

  • 引用透明性是指函数调用可以被其结果替代,而不影响程序的行为。
  • 引用透明性是由于纯函数的特性,使得函数调用可以进行推理、优化和并行化。

6、递归(Recursion):

  • 递归是函数式编程中常用的迭代控制结构。
  • 递归允许函数在自身内部调用自身,用于解决复杂的问题和数据结构。
  • 递归可以替代循环,提供了一种更简洁、优雅的方式来处理迭代问题。

7、柯里化(Currying):

  • 柯里化是函数式编程中的一种技术,它将一个接受多个参数的函数转换成一系列接受部分参数的函数链。
  • 柯里化允许我们将函数的参数进行分离和部分应用,从而创建更具复用性和灵活性的函数。
  • 提供了一种函数参数的分离和组合的方式,使函数的定义和使用更加灵活。
  • 允许通过部分应用的方式创建新的函数,提高代码的复用性。
  • 使函数的调用更加清晰和简洁,减少了参数传递的复杂性。

8、函子(Functor):

  • 函子是一种可被映射(或转换)的对象或数据结构。
  • 函子可以看作是一个容器,它封装了值,并提供了一种操作将函数应用于封装的值的方式。
  • 函子的目的是在不修改封装的值的情况下,对值进行转换、组合和操作。
  • 通过函子的转换操作,可以避免显式地进行循环和迭代,提高代码的简洁性和可读性。

9、适用函子(Applicative Functor):

  • 适用函子是一种扩展了函子的抽象,提供了一种将函数应用于函子内部值的方式。
  • 适用函子是一个抽象的数据结构或类型类,用于将具有上下文的函数应用于具有上下文的值。
  • 提供了 pure 函数,用于将普通的值包装成适用函子。
  • 提供了 <*>(也可以写作 ap)操作符或函数,用于将一个适用函子内部的函数应用于另一个适用函子内部的值。

10、单子(Monad):

  • 单子是一种抽象的设计模式,用于处理包含上下文的值。单子提供了一种结构,可以对值进行封装,并定义了一组操作来处理封装的值,同时保留了上下文信息。
  • 单子可以封装一个值,这个值可以是任意类型,比如整数、字符串、数组等。
  • 单子可以携带一些额外的上下文信息,例如错误、状态、延迟计算等。
  • 单子定义了一组操作,用于处理封装的值和上下文信息。这些操作包括转换、组合、展平等,可以对单子进行操作并生成新的单子。

这些核心概念在函数式编程中起着重要的作用,它们强调函数的纯度、不可变性和组合性,以实现可靠、可维护和可扩展的代码。这些概念使得函数式编程适用于处理并发、并行、大规模数据处理和函数组件化等领域,并成为现代编程中的重要编程范式之一。

下面以Swift为例,介绍函数式编程中的一些核心概念,以及如何应用函数式编程思想到实际开发中。

高阶函数(Higher-Order Functions)

高阶函数(Higher-Order Functions)是指能够接受函数作为参数或将函数作为返回值的函数。根据维基百科的定义,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

我们知道在Swift中函数是一等公民(First-class citizen),也就说可以将Swift中的函数作为普通的类型来使用,可以用来作为函数的参数或者返回值等。意味着,Swift中有很多高阶函数,这也是Swift能成为函数式编程语言的重要原因。常见的高阶函数有以下几种:

  • 映射函数(Mapping Functions):这类高阶函数将一个函数应用于集合中的每个元素,并返回包含结果的新集合。map 是最常见的映射函数。

  • 过滤函数(Filtering Functions):这类高阶函数接受一个函数和一个集合,根据给定函数的条件来筛选集合中的元素,并返回满足条件的新集合。filter 是常见的过滤函数。

  • 折叠函数(Folding Functions):这类高阶函数接受一个函数、一个初始值和一个集合,通过将给定函数应用于集合中的每个元素,并使用初始值和每个元素的结果进行累积运算,最终返回一个值。reduce 是常见的折叠函数。

  • 排序函数(Sorting Functions):这类高阶函数接受一个函数和一个集合,根据给定函数的规则对集合中的元素进行排序,并返回排序后的集合。sort 是常见的排序函数。

  • 条件函数(Predicate Functions):这类高阶函数接受一个函数和一个集合,并根据给定函数的条件返回一个布尔值。常见的条件函数有 all(判断集合中的所有元素是否满足给定条件)、any(判断集合中是否存在满足给定条件的元素)等。

  • 组合函数(Composition Functions):这类高阶函数接受多个函数,并返回一个新的函数,该函数按照给定函数的顺序依次调用它们。compose(或 pipe)是常见的组合函数。

  • 并行函数(Parallel Functions):这类高阶函数可以将函数应用于集合中的多个元素,并在并行环境中执行,以提高性能和效率。常见的并行函数有 map、filter 等。

这些分类仅涵盖了高阶函数的一部分,实际上还有很多其他类型的高阶函数存在,每种编程语言和函数式编程库可能都有其自己的高阶函数分类和实现。理解这些不同类型的高阶函数可以帮助开发者更好地利用函数式编程的优势和特性,编写更加简洁和灵活的代码。下面我们来了解一下Swift中一些常用的高阶函数。

map 函数与 flatMap 函数

1、map 函数
map 函数接受一个函数作为参数,并将该函数应用于集合中的每个元素,然后返回一个新的集合,其中包含对原集合中的每个元素应用给定函数后的结果。换句话说,map 将一个集合转换成另一个具有相同元素数量的集合,但是元素经过了转换。

let numbers = [1,2,3,4,5]

func double(nums: [Int]) ->[Int] {
    var arr = [Int]()
    for num in nums {
        arr.append(num * 2)
    }
    
    return arr
}

let douNums = double(nums: numbers)
// [2, 4, 6, 8, 10]
//print(douNums)

let doubleNumbers = numbers.map { $0 * 2 }
// [2, 4, 6, 8, 10]
//print(doubleNumbers)

在上述示例中,我们使用普通函数和map 函数分别将数组 numbers 中的每个元素进行2倍操作,得到一个新的数组 doubleNumbers,后者更加简洁明了。

2.flatMap 函数
flatMap 函数也接受一个函数作为参数,但与 map 不同的是,它会将函数应用于集合中的每个元素,并将结果展平为一个新的集合。这意味着,如果处理后的结果是集合类型(如数组),flatMap 会将其中的元素提取出来,组合成一个扁平的集合。

let array = ["Hello", "World"]

let flattenedArray = array.flatMap { $0.map { String($0) } }
// ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]
//print(flattenedArray)

let mapedArray = array.map { $0.map { String($0) } }
// [["H", "e", "l", "l", "o"], ["W", "o", "r", "l", "d"]]
//print(mapedArray)

在上述例子中,我们可以看到 map 和 flatMap 的区别在于它们对于结果的处理方式。map 将每个元素进行转换,并将结果放入一个新的集合中,而 flatMap 将结果展平为一个扁平的集合。

需要注意的是,flatMap 在处理可选值和可选的集合时也有特殊的行为,它会自动过滤掉了 nil 的值,从而可以方便地进行值的解包和处理。但是当闭包返回 optional值时,flatMap 函数不再支持了,取而代之的是 compactMap 函数。下面我们举例说明。

3.Optional的 map 和 flatMap

let transformedNumbers0 = numbers.map { number -> String? in
    if number < 5 {
        return nil
    } else {
        return String(number)
    }
}

// [nil, nil, nil, nil, Optional("5")]
//print(transformedNumbers0)

/*
 'flatMap' is deprecated: Please use compactMap(_:) for the case where closure returns an optional value
 */
//let transformedNumbers1 = numbers.flatMap { number -> String? in
//    if number < 5 {
//        return nil
//    } else {
//        return String(number)
//    }
//}

// ["5"]
//print(transformedNumbers1)

let transformedNumbers2 = numbers.compactMap { number -> String? in
    if number < 5 {
        return nil
    } else {
        return String(number)
    }
}

// ["5"]
//print(transformedNumbers2)

let arr = ["Tom","123","Hello","-10"]
let arr1 = arr.map { Int($0) }
//[nil, Optional(123), nil, Optional(-10)]
//print(arr1)

//let arr2 = arr.flatMap { Int($0) }
//[123, -10]
//print(arr2)

let arr3 = arr.compactMap { Int($0) }
//[123, -10]
//print(arr3)

在上面的第一个例子中,我们分别使用 map 函数、flatMap 函数和 compactMap 函数将 numbers 数组中的每个数字转换为字符串,但是只有当数字大于等于 5 时才返回一个非空的可选字符串,否则返回 nil。在上面第二个例子中,我们在分别使用这三个函数对数组 arr 中的每个字符串尝试转换为 Int 类型数组。从打印结果我们可以看出,flatMap 函数和 compactMap 函数都会自动过滤掉了返回 nil 的值,并且将可解包的 optional值自动解包。

let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd"

let str: String? = "2023-08-20"
// 旧写法
let date1 = str != nil ? fmt.date(from: str!) : nil
// 新写法
let date2 = str.flatMap(fmt.date)
//Optional(2023-08-19 16:00:00 +0000) Optional(2023-08-19 16:00:00 +0000)
//print(date1,date2)

上述例子,可以看到 flatMap 函数可以简化对可选值的解包和转换操作,并且在可选值为 nil 时直接返回 nil。这种特性让代码更加简洁和易读。flatMap 函数对于可选值的处理方式与集合稍有不同。对于可选值,flatMap 函数可以用于对其进行解包和转换操作。flatMap 的作用是将可选值 str 进行解包,并将解包后的值传递给闭包 fmt.date 进行转换操作。如果可选值 str 不为 nil,则会调用 fmt.date 方法对其进行转换,并返回结果,即一个可选的 Date 对象。如果可选值 str 为 nil,则整个表达式的结果也为 nil。

filter 函数

filter 函数接受一个闭包作为参数,该闭包定义了筛选条件。对于集合中的每个元素,闭包将被调用,并根据闭包的返回值来决定是否保留该元素。

let evenNumbers = numbers.filter { $0 % 2 == 0 }
// [2, 4]
//print(evenNumbers)

在上述示例中,我们使用 filter 函数从数组 numbers 中筛选出所有的偶数,得到一个新的数组 evenNumbers。

reduce 函数

reduce 函数接受一个初始值和一个闭包作为参数。闭包定义了一个累积操作,用于将当前累积值和集合中的每个元素进行合并,并返回一个新的累积值。最后,reduce 函数返回最终的累积值。

let sum = numbers.reduce(0) { $0 + $1 }
// 15
//print(sum)

在上述示例中,我们使用 reduce 函数将数组 numbers 中的元素相加,得到总和。

reduce 函数实现 map功能

// reduce 实现 map功能
let doubledNumbers = numbers.reduce([]) { result, element in
    result + [element * 2]
}//numbers.reduce([]) { $0 + [$1 * 2] }
// [2, 4, 6, 8, 10]
//print(doubledNumbers)

在上述代码中,使用 reduce 函数将数组 numbers 中的每个元素都乘以 2,并将结果放入一个新的数组 doubledNumbers 中。初始值 [] 表示结果数组的初始状态为空数组,闭包 { result, element in return result + [element * 2] } 对每个元素进行操作,将当前元素乘以 2 并追加到结果数组中。最终,doubledNumbers 包含了数组 numbers 中每个元素乘以 2 的结果。

reduce 函数实现 filter功能

let evenNums = numbers.reduce([]) { result, element in
    if element % 2 == 0 {
        return result + [element]
    } else {
        return result
    }
}//numbers.reduce([]) { $1 % 2 == 0 ? $0 + [$1] : $0 }
// [2, 4]
//print(evenNums)

在上述代码中,使用 reduce 函数从数组 numbers 中筛选出偶数,并将结果放入一个新的数组 evenNums 中。初始值 [] 表示结果数组的初始状态为空数组,闭包 { result, element in if element % 2 == 0 { return result + [element] } else { return result } } 对每个元素进行操作,如果当前元素是偶数,则将其追加到结果数组中,否则忽略。最终,evenNums 包含了数组 numbers 中的所有偶数。

更多 reduce 函数使用示例

// 使用 reduce 计算最大值
let maxNum = numbers.reduce(numbers[0]) { $1 > $0 ? $1 : $0 }//{ max, element in  element > max ? element : max }
// 5
//print(maxNum)

// 使用 reduce 进行字符串拼接
let words = ["Hello", " ", "World", "!"]
let sentence = words.reduce("") { $0 + $1 }//{ result, word in return result + word }
// "Hello World!"
//print(sentence)

在上述代码中,使用 reduce 函数计算数组 numbers 中的最大值;将字符串数组 words 中的元素进行拼接,生成一个完整的句子。

这些示例展示了Swift中常用高阶函数的用法。它们接受函数作为参数,将函数作为一等公民来处理和操作数据。高阶函数可以提高代码的可读性和简洁性,并使我们能够以声明性的方式进行数据转换、筛选和聚合等操作。

其它高阶函数

  • sorted:根据给定的排序闭包对集合中的元素进行排序,并返回排序后的新集合。
  • allSatisfy:判断集合中的所有元素是否都满足给定的条件。
  • contains:判断集合中是否存在满足给定条件的元素。
  • zip:将两个集合的对应元素进行配对,返回一个由元组组成的新集合。
// allSatisfy 函数
let allPositive = numbers.allSatisfy { $0 > 0 }//{ number in return number > 0 }
// 结果为 true
//print(allPositive)

纯函数(Pure Functions)

纯函数(Pure Functions)是指具备以下两个特性的函数:

  1. 相同的输入总是产生相同的输出。
  2. 在函数的执行过程中没有副作用,即不会修改外部状态或产生可观察的变化。
    纯函数是函数式编程中的重要概念,它们具有可预测性、可测试性和可组合性等优点。
func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

let result = add(3, 4)  // 结果为 7

在上述示例中,add 函数接受两个整数作为输入,并返回它们的和。这个函数满足纯函数的两个特性:相同的输入(例如 3 和 4)总是会产生相同的输出(例如 7),而且函数的执行过程中没有副作用。

非纯函数(Impure Functions)

非纯函数(Impure Function)是指具有副作用或依赖于外部状态的函数。与纯函数不同,非纯函数的输出不仅仅由输入决定,还可能受到其他因素的影响,例如修改全局变量、读写文件、进行网络请求等。

var total = 0

func addToTotal(_ value: Int) {
    total += value
}

addToTotal(5)
print(total)  // 输出: 5

addToTotal(3)
print(total)  // 输出: 8

在上述示例中,我们有一个全局变量 total 和一个名为 addToTotal 的函数,该函数接受一个整数值并将其添加到 total 变量上。这个函数就不是纯函数,因为它对外部状态(即 total 变量)进行了修改,产生了可观察的变化,也即产生了副作用。

副作用(Side Effect):
副作用是指函数在执行过程中对函数外部环境产生的可观察的变化。副作用可能包括修改全局变量、修改可变数据结构、I/O 操作、抛出异常等。

主要特点:

  • 修改外部状态或可变数据。
  • 进行 I/O 操作,如读写文件、网络请求等。
  • 抛出异常或改变程序的控制流。

具有副作用的函数可能会引入一些问题:

  1. 难以测试:由于副作用会改变程序的状态,测试具有副作用的函数变得更加困难。为了验证函数的正确性,我们需要模拟和控制副作用的发生,或者依赖于特定的环境。这增加了测试的复杂性和不确定性。
  2. 难以理解和推理:具有副作用的函数可能会在不同的上下文中产生不同的行为,这使得代码的行为不够明确和可预测。阅读和推理这样的函数变得更加困难,因为我们不仅需要关注函数本身的逻辑,还需要了解可能的副作用和它们的影响。
  3. 可能引入错误:由于副作用的影响不仅限于函数本身,在具有副作用的代码中修改全局状态或可变数据结构可能导致意外的结果。这种不受控制的状态变化可能引入难以调试和修复的错误。

函数式编程通过强调使用纯函数来避免或最小化副作用的影响。纯函数的引用透明性使得函数的行为更可靠和可预测,简化了测试、理解和推理代码的过程。通过将副作用局限在特定的边界内,函数式编程提倡使用不可变数据和明确的数据流来构建可靠的软件系统。

纯函数的特点

纯函数的优点在于,它们的行为可预测且不依赖于外部状态。这使得纯函数易于测试、调试和组合,同时也有助于编写更可靠和可维护的代码。通过遵循纯函数的原则,我们可以减少程序中的不确定性,并更好地控制代码的行为。

除了可预测性和无副作用之外,纯函数在函数式编程中还具有以下重要的特点:

  1. 可组合性(Composability):纯函数能够轻松地组合到更复杂的函数中,形成函数链或函数组合。这种组合性使得代码更具可读性、可重用性和可扩展性。通过将多个纯函数组合起来,我们可以构建出更高层次的抽象,而无需关注函数内部的实现细节。
  2. 引用透明性(Referential Transparency):纯函数在程序中的任何地方都可以被其返回值所替代,而不会对程序的行为产生影响。这种引用透明性使得代码更容易推理、理解和优化。通过将纯函数视为黑盒子,我们可以更轻松地分析程序的行为,而无需考虑函数的上下文或依赖关系。
  3. 缓存优化(Memoization):由于纯函数的输出仅取决于输入,因此可以使用缓存来优化函数的执行。通过缓存函数的输入和输出对应关系,可以避免重复计算,提高性能。这在处理复杂且耗时的计算时特别有用,因为只有在输入发生变化时才会重新计算结果。
  4. 并行和并发性(Parallel and Concurrent Execution):由于纯函数没有副作用,它们可以安全地在并行和并发环境中执行,而无需担心竞态条件或共享状态的问题。这使得函数式编程在并行计算和多线程环境中具有天然的优势,并有助于编写高效且线程安全的代码。

这些特点使纯函数成为函数式编程范式的核心要素。通过遵循纯函数的原则,我们能够编写更简洁、可读性更高、可测试性更强且更具可扩展性的代码。此外,纯函数的特性还为函数式编程提供了许多有用的工具和技术,如函数合成、偏函数应用、柯里化等,这些可以进一步提升代码的灵活性和表达能力。

引用透明性(Referential Transparency)

纯函数的引用透明性(Referential Transparency)指的是函数的返回值可以在程序的任何地方替代函数本身,而不会对程序的行为产生影响。简单来说,相同的输入总是得到相同的输出,无论在何处使用。这种特性有助于我们更好地理解和推理代码的行为,具体有以下几个方面的影响:

  1. 可替代性:由于纯函数的返回值可以在任何地方替代函数本身,我们可以将函数调用结果视为一个常量。这使得代码更容易理解,因为我们可以将复杂的函数调用链简化为对应的结果值,而不必关注函数内部的实现细节。
  2. 可推理性:纯函数的引用透明性让我们可以更容易地推理代码的行为。我们可以根据函数的输入和输出之间的关系来分析代码,而无需考虑函数的上下文或外部状态的影响。这种透明性可以帮助我们更准确地预测代码的行为并进行调试和优化。
  3. 简化测试:由于纯函数的行为仅取决于输入,我们可以更轻松地编写测试用例来验证函数的正确性。我们只需要关注输入和期望的输出,而无需考虑外部状态或与其他函数的交互。这种简化的测试方式使得测试更容易编写、执行和维护。
  4. 容易优化:纯函数的引用透明性使得编译器和运行时环境可以进行更自由的优化。由于函数的结果只依赖于输入,编译器可以进行常量折叠、公共子表达式消除等优化,而无需担心副作用的影响。这可以提高代码的执行效率和性能。

总体而言,纯函数的引用透明性提供了一种简化和抽象的方式来理解和推理代码。通过将纯函数视为黑盒子,我们可以更专注地分析函数的输入和输出之间的关系,而无需关注函数的具体实现细节或与其他部分的交互。这种思维方式使得代码更清晰、更易于维护,同时也提供了更高的可靠性和可扩展性。

纯函数的引用透明性对代码的可测试性和可维护性有着重要的影响,具体来说:

  • 可测试性:由于纯函数的行为仅取决于输入,而不依赖于外部状态或产生副作用,我们可以更轻松地编写测试用例来验证函数的正确性。在测试纯函数时,我们只需要提供输入,并验证输出是否符合预期。这种简化的测试方式使得测试更容易编写、执行和维护,而且测试结果更可靠。我们可以更自信地修改纯函数的实现,而无需担心对其他代码产生不良影响。
  • 可维护性:纯函数的引用透明性使得函数的行为更加可预测和可靠。函数的结果只依赖于输入,而不受外部状态影响,这简化了代码的理解和维护。我们可以将纯函数视为独立的模块,只需关注函数的输入和输出之间的关系,而无需关心函数内部的实现细节。这种模块化的设计使得代码更易读,更易于修改和调试,降低了代码的复杂性。
  • 可重用性:纯函数具有很高的可重用性。由于纯函数仅依赖于输入,而不修改外部状态,它们可以在不同的上下文中被安全地使用和组合。这种可组合性使得我们可以将纯函数作为构建块,组合成更复杂的函数和模块,从而提高代码的可重用性。我们可以将纯函数抽象为通用的功能,供多个地方使用,而无需担心副作用或外部状态的干扰。

总的来说,纯函数的引用透明性提供了一种更可测试和可维护的编程方式。通过遵循纯函数的原则,我们能够编写更独立、可预测且易于测试的代码。这种设计风格使得代码更容易理解、修改和扩展,提高了代码的可维护性,并促进了代码的重用和组合。

以下是一个示例,展示了如何在实际开发中使用引用透明性来简化代码和增加可测试性。假设我们正在开发一个电子商务App,其中有一个购物车功能,需要实现一个函数来计算购物车中所有商品的总价。我们可以使用引用透明性来编写这个函数。

首先,我们定义一个表示商品的结构体:

struct Product {
    let name: String
    let price: Double
}

然后,我们定义一个函数来计算购物车中所有商品的总价:

func calculateTotalPrice(products: [Product]) -> Double {
    return products.reduce(0.0) { $0 + $1.price }
}

在这个例子中,函数calculateTotalPrice接收一个包含商品的数组作为输入,并返回这些商品的总价。这个函数是纯函数,因为它没有副作用,并且对于相同的输入,总是返回相同的输出。

现在,我们可以利用引用透明性的特性来简化代码和增加可测试性。假设我们有一个购物车实例,并且有一些商品已经被添加到购物车中:

let cart = [Product(name: "iPhone", price: 999.0),
            Product(name: "MacBook", price: 1999.0),
            Product(name: "AirPods", price: 199.0)]

我们可以直接调用calculateTotalPrice函数来计算购物车中所有商品的总价:

let totalPrice = calculateTotalPrice(products: cart)
print(totalPrice) // 输出:3197.0

由于引用透明性,我们可以将函数调用的结果视为一个常量,可以在代码的任何地方使用这个常量,而不必关注函数的具体实现。这使得代码更加简洁和易读。

此外,引用透明性还使得我们能够轻松地编写测试用例来验证函数的正确性。我们只需要提供不同的输入,并验证输出是否符合预期。例如,我们可以编写以下测试用例:

let testCart = [Product(name: "Shoes", price: 99.0),
                Product(name: "T-Shirt", price: 29.0)]

let totalPrice1 = calculateTotalPrice(products: testCart)
assert(totalPrice1 == 128.0, "Incorrect total price")

通过引用透明性,我们可以更方便地推理和测试代码的行为,并且可以轻松地修改和优化函数的实现,而无需担心对其他部分的影响。这个例子展示了在实际开发中如何使用引用透明性来简化代码和提高可测试性。通过将函数的行为仅限于输入和输出之间的关系,我们能够更好地理解和推理代码,并提供更可靠的软件解决方案。

不可变数据(Immutable Data)

在函数式编程中,不可变数据是一个重要的概念,它强调对数据的不可变性和避免副作用的原则。不可变数据意味着一旦数据被创建,就不能再被修改。不可变数据是纯函数的基础。

下面通过一个例子来说明在 Swift 函数式编程中如何体现不可变数据的概念。
假设我们有一个包含学生信息的数组,每个学生有姓名和年龄两个属性。我们想要筛选出年龄大于等于 18 岁的学生并进行打印。

首先,我们定义一个学生结构体,用于表示学生的信息:

struct Student {
    let name: String
    let age: Int
}

接下来,我们创建一个包含学生信息的数组:

let students = [
    Student(name: "Alice", age: 20),
    Student(name: "Bob", age: 17),
    Student(name: "Charlie", age: 19),
    Student(name: "David", age: 22)
]

现在,我们可以使用函数式编程的方法来筛选出年龄大于等于 18 岁的学生,并进行打印。在函数式编程中,我们通常使用高阶函数 filter 来对数组进行筛选操作。

let adults = students.filter { $0.age >= 18 }

在这个例子中,我们使用了 filter 函数来筛选 students 数组,并保留满足条件 $$0.age >= 18 的学生。注意,我们在闭包中使用了不可变参数 $0 来表示数组中的每个元素,这是因为在函数式编程中,我们强调不可变性,避免对数据进行修改。
最后,我们可以对 adults 数组进行打印操作:

adults.forEach { print($0.name) }

通过使用不可变数据的概念,我们确保了原始的 students 数组不会被修改,而是创建了一个新的数组 adults 来存储满足条件的学生。这符合函数式编程中的不可变性原则,避免了副作用和意外的数据修改。

总结来说,在 Swift 函数式编程中,不可变数据是通过使用 let 关键字来声明不可变的变量或属性,强调数据的不可变性。通过使用不可变数据,我们可以避免副作用,提高代码的可靠性和可维护性。同时,函数式编程中的许多高阶函数(如 map、filter、reduce 等)都鼓励对不可变数据进行操作,以实现函数的纯粹性和可组合性。

不可变性原则的好处

函数式编程中的不可变性原则带来了许多好处,包括以下几个方面:

  1. 避免副作用:不可变性确保数据在创建后无法被修改,从而避免了副作用的产生。副作用是指对数据进行修改或对外部环境产生影响的操作,它使得代码的行为不可预测,增加了代码的复杂性和错误的可能性。通过使用不可变数据,函数式编程限制了对数据的修改,从而减少了副作用带来的问题。
  2. 纯函数:不可变性是实现纯函数的基础。纯函数是指具有相同输入总是产生相同输出,并且没有副作用的函数。由于不可变数据的特性,纯函数的输出仅依赖于输入参数,不受外部状态的影响,因此更容易理解和测试,且具有更好的可组合性。
  3. 线程安全:不可变性使得函数式编程中的数据在多线程环境下更容易管理和操作。由于不可变数据不会被修改,不存在竞争条件和数据访问冲突的问题。这样可以避免线程安全的复杂性,减少并发编程中的错误和调试难度。
  4. 可缓存性:不可变数据具有可缓存性,即函数的输出结果可以缓存起来以供重复使用。由于纯函数的输出仅依赖于输入参数,相同的输入总是会得到相同的输出。这使得函数的结果可以缓存起来,避免重复计算,提高程序的性能。
  5. 可回溯性和调试:由于不可变数据不会被修改,每次操作都会生成一个新的数据副本,形成一个不可变数据的历史记录。这使得可以轻松地回溯到先前的状态,并进行调试和错误排查。不可变性提供了数据的可追溯性,方便调试复杂的函数流程。

总之,不可变性原则在函数式编程中起到了重要的作用。它促进了代码的清晰性、可测试性和可维护性,减少了副作用和数据访问冲突的问题,提高了程序的性能和并发编程的可靠性。

实现不可变性的关键点

在函数式编程中,实现不可变性的关键有以下几个方面:

  1. 使用不可变的数据结构:函数式编程中使用不可变的数据结构,例如不可变的列表、集合、字典等。这些数据结构在创建后不能被修改,任何对其的操作都会返回一个新的不可变数据结构,保持原始数据的不可变性。
  2. 使用不可变的变量或常量:在函数式编程中,尽量使用不可变的变量或常量。在 Swift 中,可以使用 let 关键字声明不可变常量,确保其赋值后不能再修改。这样可以防止意外的修改和副作用。
  3. 避免可变状态:尽量避免使用可变的状态,尤其是全局的可变状态。可变状态会引入副作用和数据共享的问题,降低代码的可靠性和可维护性。函数式编程鼓励将状态转换为不可变的数据,并通过函数的组合和传递来处理状态的变化。
  4. 使用纯函数:纯函数是指具有相同输入总是产生相同输出,并且没有副作用的函数。纯函数不会修改传入的参数,也不会依赖外部状态。通过使用纯函数,可以保持数据的不可变性,避免副作用和数据修改的问题。
  5. 函数组合和高阶函数:函数式编程中的函数组合和高阶函数可以避免对数据进行直接修改。通过将多个函数组合在一起,每个函数只负责一个简单的操作,并返回一个新的不可变结果。这种方式避免了对原始数据的修改,保持了数据的不可变性。
  6. 不可变性的区分和传递:在函数式编程中,不可变性是通过将不可变数据传递给函数来实现的。当需要对数据进行操作时,函数产生一个新的不可变数据结构,并将其传递给下一个函数。这种方式确保了中间数据的不可变性,并将结果传递给下一个操作,避免了数据的修改。

总结来说,函数式编程中实现不可变性的关键在于使用不可变的数据结构、不可变的变量或常量,避免可变状态,使用纯函数,以及通过函数组合和高阶函数来处理数据。这些原则和技术共同确保了数据的不可变性,提高了代码的可靠性、可测试性和可维护性。

纯函数和不可变数据的关系

纯函数和不可变数据相互支持和加强彼此的特性,具体体现在以下几个方面:

  1. 纯函数操作不可变数据:纯函数的特性要求函数不会修改传入的参数,而只是根据输入产生一个新的输出。当函数操作不可变数据时,它可以放心地处理数据,不必担心数据被意外修改。这增强了纯函数的可靠性和可维护性。
  2. 不可变数据作为纯函数的输入:不可变数据作为纯函数的输入,保证了函数的输入参数不会被修改。这意味着纯函数的输出只取决于输入参数,不受外部状态的影响,使得纯函数更容易理解、测试和推理。
  3. 不可变数据的传递和复用:不可变数据的特性使得它们可以被安全地传递给其他函数或模块,而不必担心数据被修改。通过将不可变数据传递给纯函数,可以避免副作用和数据共享的问题,确保函数的结果只依赖于输入参数。
  4. 不可变数据的缓存和共享:由于不可变数据的特性,纯函数的输出结果可以被缓存起来以供重复使用。相同的输入总是会产生相同的输出,这使得可以共享和复用纯函数的结果,提高程序的性能和效率。

综上所述,纯函数和不可变数据在函数式编程中是相辅相成的。纯函数操作不可变数据,不可变数据作为纯函数的输入,它们共同确保了函数式编程的可靠性、可测试性和可维护性。同时,不可变数据的特性也增强了纯函数的可靠性和性能。

柯里化(Currying)

柯里化(Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。简言之,柯里化是一种函数转换技术。

下面我们以Swift为例,说明如何对一个函数进行柯里化操作。

1、将两个整数相加的函数进行柯里化

//普通形式
//func add(_ a: Int, _ b: Int) -> Int {
//    return a + b
//}
//let num0 = add(3, 5); // 8

//柯里化形式
func add(_ x: Int) -> (Int) -> Int {
    return { y in
        return y + x
    }
}

let add3 = add(3)  // 创建一个新函数,将 3 添加到参数上
let num1 = add3(5) // 调用新函数,将 5 添加到参数上,得到结果 8

let num = add(3)(5) // 8

在这个例子中,add 函数接受一个整数 x,并返回一个接受整数 y 的函数,该函数将 x 和 y 相加并返回结果。通过调用 add(3),我们创建了一个新函数 add3,它将 3 添加到参数上。然后,通过调用 add3(5),我们将 5 添加到参数上,并得到结果 8。

  1. 使用柯里化创建自定义的过滤函数:
func filter(with condition: @escaping (Int) -> Bool) -> ([Int]) -> [Int] {
    return { numbers in
        return numbers.filter(condition)
    }
}

let evenFilter = filter { $0 % 2 == 0 } // 创建一个新函数,用于过滤偶数
let filteredNumbers = evenFilter(numbers) // 调用新函数,过滤出偶数 [2, 4]
//print(filteredNumbers)

在这个例子中,filter 函数接受一个接受整数并返回布尔值的条件函数,并返回一个接受整数数组并返回过滤结果的函数。我们使用柯里化创建了一个新函数 evenFilter,它使用条件函数 { $0 % 2 == 0 } 来过滤偶数。然后,通过调用 evenFilter 并传入整数数组 numbers,我们得到了过滤出偶数的结果 [2, 4]。

3.创建一个通用型的柯里化函数

func curry<A, B, C>(_ function: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { a in { b in function(a, b) } }
}

这个通用的柯里化函数接受一个类型为 (A, B) -> C 的函数作为输入,并返回一个类型为 A -> B -> C 的柯里化函数。
让我们通过一个例子来说明如何使用这个通用柯里化函数。假设我们有一个接受两个参数的函数 add ,用于将两个整数相加。现在,我们可以使用 curry 函数将这个函数转换为柯里化的形式:

let curriedAdd = curry(add(_:_:))

现在,curriedAdd 是一个柯里化后的函数,它接受一个整数作为输入,并返回另一个函数,该函数接受另一个整数作为输入,并返回它们的和。
我们可以使用柯里化后的函数来进行部分应用,例如:

let addFive = curriedAdd(5)
let addThree = addFive(3) // 相当于调用 add(5, 3)
//print(addThree) // 输出结果 8

在这个例子中,我们首先使用 curry 函数将 add 函数转换为柯里化后的形式。然后,我们使用柯里化后的函数 curriedAdd 进行部分应用,将第一个参数设置为 5。最后,我们将第二个参数 3 应用到部分应用后的函数上,得到最终的结果。

通过柯里化,我们可以更方便地进行函数组合和部分应用,使代码更具可读性和可组合性。通用的柯里化函数提供了一种灵活的方式来实现这种转换,并在需要时将任意形式的函数转换为柯里化形式。

除了柯里化之外,函数式编程还有其他一些常见的函数转换技术。下面介绍其中的两种:反柯里化和部分应用。

  1. 反柯里化(Uncurrying):反柯里化是柯里化的逆过程。它将柯里化后的函数转换回原始的接受多个参数的函数形式。这种转换使得我们可以将原本以柯里化形式定义的函数,转换为接受多个参数的函数,从而更方便地进行调用。在某些编程语言中,可以使用特定的语法或库函数来进行反柯里化。
  2. 部分应用(Partial Application):部分应用是指通过固定函数的某些参数,创建一个新的函数,该函数接受剩余的参数并执行原始函数。部分应用使得我们可以在调用函数之前设置一部分参数,从而创建一个新的函数,该函数可以在稍后的时间点进行调用。这种转换在函数式编程中非常有用,可以帮助我们减少重复的代码,提高代码的可重用性。

这些函数转换技术在函数式编程中非常常见,并且它们可以相互结合使用,以便在不同的场景下提供更强大的编程能力。选择使用哪种技术取决于具体的需求和编程语言的支持情况。无论是柯里化、反柯里化还是部分应用,它们都提供了一种将函数转换为不同形式的方式,以适应不同的编程需求。

反柯里化(Uncurrying)

我们将第一个柯里化的函数 add(该接受两个整数作为参数并返回它们的和)反柯里化,将其转换为接受两个参数的函数:

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

let addFn = uncurriedAdd
let tempNum = addFn(3,5)
//print(tempNum) // 8

通过反柯里化,我们将柯里化的函数 add 转换为了接受两个参数的函数 uncurriedAdd。

部分应用(Partial Application):

假设我们有一个接受三个参数的函数 multiply,用于计算三个整数的乘积:

func multiply(_ a: Int, _ b: Int, _ c: Int) -> Int {
    return a * b * c
}

let multiplyByTwo: (Int, Int) -> Int = { multiply(2, $0, $1) }
tempNum = multiplyByTwo(3,4) //相当于调用 multiply(2, 3, 4)
//print(tempNum) //4

在上述示例中,我们使用闭包 { multiply(2, $0, $1) } 将函数 multiply 部分应用到参数 2 上,并将这个闭包赋值给变量 multiplyByTwo。

最后,我们调用 multiplyByTwo 函数,传入参数 3 和 4,得到结果 24。

补充说明,在 Python 中,可以使用 functools.partial 函数实现部分应用函数。

from functools import partial

def multiply(a, b, c):
    return a * b * c

multiply_by_two = partial(multiply, 2)
result = multiply_by_two(3, 4)  # 相当于调用multiply(2,3,4) 结果为 24

在 Swift 中,无法使用下划线 _ 来表示省略参数标签的方式进行部分应用函数。

部分应用技术在以下情况下非常有用:

  • 延迟执行:通过部分应用函数,可以将函数的一部分参数提前固定,延迟执行该函数。这对于需要在稍后的时间点或特定条件下执行函数非常有用。例如,在事件处理程序中,可以使用部分应用函数来预先设置某些参数,以便在事件发生时调用该函数。

  • 参数复用:当一个函数有多个参数,而某些参数在不同的函数调用中保持不变时,部分应用函数可以帮助消除重复的参数传递。它允许我们创建一个新的函数,其中某些参数已经被固定,从而简化了函数调用的语法。

  • 函数组合:部分应用函数是函数组合的重要工具。通过将部分应用函数与其他函数组合,可以创建出更复杂的函数,以满足特定的需求。这种组合能够提高代码的可读性和模块化,并促进代码重用。

优势:

  • 代码简洁性:部分应用函数可以减少重复的代码,提高代码的可读性和简洁性。通过固定某些参数,我们可以将函数调用的语法变得更简洁,减少冗余的参数传递。

  • 可复用性:通过部分应用函数,我们可以创建出新的函数,该函数固定了一部分参数。这种函数的可复用性较高,可以在多个上下文中使用,避免了重复编写相似代码的问题。

  • 灵活性:部分应用函数提供了灵活性,允许我们在函数调用中动态地固定参数。这使得我们可以根据需要创建不同的函数变体,以适应不同的使用场景。

  • 函数组合:部分应用函数是函数组合的重要工具。通过将部分应用函数与其他函数组合,我们可以创建出更复杂的函数,以满足特定的需求。这种组合能够提高代码的可读性和模块化,并促进代码重用。

总而言之,部分应用技术允许我们以更简洁、可复用和灵活的方式处理函数调用。它在延迟执行、参数复用和函数组合等场景中都非常有用,并提供了更清晰和模块化的代码结构。

函数合成

函数合成是一种函数式编程的技术,它允许我们将多个函数组合成一个更复杂的函数。在Swift中,我们可以使用高阶函数和函数组合操作符来实现函数合成。

假设我们有以下两个简单的函数:

func addOne(_ x: Int) -> Int {
    return x + 1
}

func double(_ x: Int) -> Int {
    return x * 2
}

现在,我们希望将这两个函数进行合成,构建一个新的函数,该函数首先将输入值加一,然后将结果乘以2。下面是详细的合成过程:

  • 步骤 1: 使用高阶函数 compose 定义函数合成操作符

假定A、B、C之间的关系为:A——g(x)——>B——f(x)——>C

首先,我们可以定义一个高阶函数 compose,它接收两个函数作为输入,并返回一个新的函数,该函数将输入值应用于第一个函数,然后将结果应用于第二个函数。在Swift中,我们可以使用泛型和函数类型来定义这个函数:

func compose<A, B, C>(_ f: @escaping (B) -> C, _ g: @escaping (A) -> B) -> (A) -> C {
    return { x in
        f(g(x))
    }
}

在这个函数的定义中,A、B、C 是泛型类型参数,用于表示函数 compose 的输入参数和输出结果的类型。

函数 compose 的定义中,首先有 _ f: @escaping (B) -> C 和 _ g: @escaping (A) -> B 这两个参数。这两个参数的顺序决定了函数 compose 的参数顺序。

根据函数的定义,我们可以知道 A、B、C 各自的含义:

  • A:表示函数 g 的输入参数类型。这个参数类型会传递给函数 g,并生成类型为 B 的结果。
  • B:表示函数 g 的输出结果类型,同时也是函数 f 的输入参数类型。函数 f 将使用这个输入参数类型生成类型为 C 的结果。
  • C:表示函数 f 的输出结果类型,也是最终 compose 函数的输出结果类型。

换句话说,这个高阶函数 compose 返回一个函数,这个函数接受类型为 A 的参数并返回类型为 C 的结果。这个返回的函数实际上就是函数合成后的新函数,它将参数 x 传递给函数 g,然后将 g 的结果传递给函数 f,最终得到类型为 C 的结果。

  • 步骤 2: 使用 compose 合成函数

接下来,我们使用 compose 函数将 addOne 和 double 函数进行合成,创建一个新的函数:

let composedFunction = compose(double, addOne)

现在,composedFunction 是一个合成函数,它将输入值应用于 addOne 函数,然后将结果应用于 double 函数。

  • 步骤 3: 调用合成函数

最后,我们可以调用合成函数并查看结果:

tempNum = composedFunction(3)
//print(tempNum)  // 输出:8

在这个例子中,我们将输入值 3 应用于 composedFunction,它首先将 3 传递给 addOne 函数,得到结果 4,然后将 4 传递给 double 函数,最终得到结果 8。

通过函数合成,我们可以将多个简单函数组合成一个更复杂的函数,从而提高代码的可读性和可维护性。在这个例子中,我们使用了 compose 函数和函数组合操作符来实现函数合成,但在实际开发中,Swift中的函数合成还可以通过其他方式实现,例如使用 reduce 函数或自定义操作符。

结合律

纯函数是函数合成的基础。函数的合成必须满足结合律在这里插入图片描述

在函数合成中,结合律是一个重要的性质。如果我们希望函数合成满足结合律,那就意味着函数的组合顺序不会影响最终的结果。换句话说,无论我们如何组合函数,最终的效果应该是相同的。

假定A、B、C、D之间的关系为:A——f(x)——>B——g(x)——>C——h(x)——>D
那么它们之间的合成有如下等价关系:

compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)

现在,我们在上面两个函数合成的基础上再添加一个函数:

func subtractThree(_ x: Int) -> Int {
    return x - 3
}

我们将这些三个函数进行函数合成,并尝试不同的组合顺序,以验证是否满足结合律。

方式 1: 不满足结合律

let result1 = subtractThree(double(addOne(5)))
let result2 = subtractThree(addOne(double(5)))

//print(result1)  // 输出:9  
//print(result2)  // 输出:8  

在这种情况下,我们先应用 addOne 函数,然后是 double 函数,最后是 subtractThree 函数。结果是 9。
但是,如果我们改变函数的组合顺序,先应用 double 函数后再应用 addOne 函数,最后是 subtractThree 函数,结果将变为 8。
这说明函数的组合顺序会影响最终结果,因此不满足结合律。

方式 2: 满足结合律

let composedFunction1 = compose(subtractThree, compose(double, addOne))
let composedFunction2 = compose(compose(subtractThree, double), addOne)
let composedFunction3 = compose(subtractThree, double, addOne)

let result3 = composedFunction1(5)
let result4 = composedFunction2(5)
let result5 = composedFunction3(5)

//print(result3)  // 输出:9
//print(result4)  // 输出:9
//print(result5)  // 输出:9

在这种情况下,我们使用 compose 函数将函数进行合成,并确保函数的组合顺序是相同的。三种情况输出相同。
这表明函数的组合顺序不会影响最终结果,因此满足结合律。

结合律的满足使得我们可以自由地对函数进行组合,并且可以按照自己的喜好和需求进行组合顺序的选择。这提供了更大的灵活性和可组合性,使函数合成成为一种强大的工具,在函数式编程中被广泛使用。

函数合成除了结合律,还具有其他重要的性质,如单位元、可逆性等。

单位元

在函数合成中,单位元是指一个特殊的函数,它不对输入值进行任何处理并将其原样返回。在函数合成中,单位元函数在组合函数链中起到特殊的作用,类似于数学中的乘法中的单位元1或加法中的零元0。

在Swift中,可以通过一个简单的函数来表示单位元,该函数接受一个输入值并将其返回,而不进行任何操作。下面是一个示例,使用Swift语言定义一个单位元函数:

func identity<T>(_ value: T) -> T {
    return value
}

在上述示例中,identity 函数是一个泛型函数,接受一个值 value 作为输入,并将其原样返回。这个函数不对输入值进行任何处理,只是简单地返回输入值本身。

下面是一个使用单位元函数的函数合成示例:

let add1 = { (x: Int) -> Int in
    return x + 1
}

let multiplyBy2 = { (x: Int) -> Int in
    return x * 2
}

//以下五句等价
let composed = compose(identity, add1, multiplyBy2, identity)
//let composed = compose(add1, multiplyBy2, identity)
//let composed = compose(identity, add1, multiplyBy2)
//let composed = compose(add1, multiplyBy2)
//let composed = compose(identity, add1, identity, multiplyBy2, identity)

tempNum = composed(3)  // 结果为 7
//print(tempNum)

在上述示例中,我们定义了两个简单的函数 add1 和 multiplyBy2,以及一个单位元函数 identity。然后,我们使用 compose 函数将它们组合成一个函数链,并将输入值 3 应用于该函数链。最终的结果都是 7。只要 add1 和 multiplyBy2 两个在函数链中的相对位置不变,无论加多少单位元函数 identity,最终结果都是一样。

可逆性

函数合成是可逆的,这意味着我们可以通过合成逆向的函数来撤销合成操作。如果我们有函数 f 和 g,并将它们合成为函数 h,那么我们可以找到逆函数 h’,它将 h 的结果反向应用到 f 和 g 上。换句话说,h’ 是 h 的逆向函数。

例如,如果我们有函数 f 和 g,并将它们合成为函数 h,我们可以找到逆函数 h’,使得 h’ 和 h 的合成等于单位元函数:

let h: (A) -> C = compose(f, g)
let h': (C) -> A = compose(g', f')
let identity: (A) -> A = compose(h', h)  // identity 函数

这个性质使得函数合成具有可逆性。

通过函数合成和使用逆函数,我们能够实现函数操作的可逆性。这样的可逆性在某些情况下非常有用,例如在数据转换、状态管理等领域。它提供了一种便捷的方式来操作和恢复数据状态,同时保持代码的简洁和可读性。下面举个例子说明这一性质的应用:

假设有一个程序,需要处理一个用户的个人资料,并提供一些操作来修改用户的信息。我们可以使用函数组合来构建这些操作,并确保可以轻松地回到初始状态。

struct UserProfile {
    var name: String
    var age: Int
    var email: String
}

func setName(_ newName: String) -> (UserProfile) -> UserProfile {
    return { user in
        var updatedUser = user
        updatedUser.name = newName
        return updatedUser
    }
}

func setAge(_ newAge: Int) -> (UserProfile) -> UserProfile {
    return { user in
        var updatedUser = user
        updatedUser.age = newAge
        return updatedUser
    }
}

func setEmail(_ newEmail: String) -> (UserProfile) -> UserProfile {
    return { user in
        var updatedUser = user
        updatedUser.email = newEmail
        return updatedUser
    }
}

let updateProfile = compose(setName("David"), setAge(35), setEmail("david@example.com"))

var user = UserProfile(name: "John", age: 30, email: "john@example.com")
user = updateProfile(user)
//print(user) //UserProfile(name: "David", age: 35, email: "david@example.com")

let resetProfile = compose(setName("John"), setAge(30), setEmail("john@example.com"))
user = resetProfile(user)
//print(user) //UserProfile(name: "John", age: 30, email: "john@example.com")

在上述示例中,我们定义了一个表示用户个人资料的数据结构和一些用于修改用户信息的函数,使用函数组合来创建更复杂的操作,比如:updateProfile 函数更新用户的个人资料以及 resetProfile 函数来回滚用户的个人资料。

通过将可逆的函数组合起来,我们可以轻松地实现复杂操作的回滚,将数据恢复到初始状态。这种可逆性为我们提供了更大的灵活性和功能,使我们能够处理复杂的操作和状态管理。

函数式编程简单实例

假定我们要实现如下功能:[(num + 3) * 5 - 1] % 10 / 2。首先,我们使用最简单粗暴的方式实现这个功能,也就是直接定义五个函数计算。如下所示:

// 加法函数
func add(_ v1: Int, _ v2: Int) -> Int {
    return v1 + v2
}

// 减法函数
func subtract(_ v1: Int, _ v2: Int) -> Int {
    return v1 - v2
}

// 乘法函数
func multiply(_ v1: Int, _ v2: Int) -> Int {
    return v1 * v2
}

// 除法函数
func divide(_ v1: Int, _ v2: Int) -> Int {
    return v1 / v2
}

// 取模函数
func modulo(_ v1: Int, _ v2: Int) -> Int {
    return v1 % v2
}

// [(num + 3) * 5 - 1] % 10 / 2
let num = 10
let result = divide(modulo(subtract(multiply(add(num, 3), 5), 1), 10), 2)

print(result) // 2

如上所示,这种方式虽然实现了功能,但是很不优雅,嵌套层次太深。下面通过使用函数柯里化和函数组合等技术,我们可以将多个函数以流水线的形式连接起来,优雅地实现相同功能。

首先,将上面五个函数分别柯里化:

// 加法函数
func add(_ v1: Int) -> (Int) -> Int {
    return { v2 in v2 + v1 }
}
    
// 减法函数
func subtract(_ v1: Int) -> (Int) -> Int {
    return { v2 in v2 - v1 }
}

// 乘法函数
func multiply(_ v1: Int) -> (Int) -> Int {
    return { v2 in v2 * v1 }
}

// 除法函数
func divide(_ v1: Int) -> (Int) -> Int {
    return { v2 in v2 / v1 }
}

// 取模函数
func modulo(_ v1: Int) -> (Int) -> Int {
    return { v2 in v2 % v1 }
}

然后使用自定义运算符 >>> 将五个函数合成为一个通用函数:

//使用自定义运算符 >>> 将五个函数合成为一个通用函数
precedencegroup FunctionComposition { //结合性
    associativity: left
}

infix operator >>>: FunctionComposition

func >>> <A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
    return { x in
        g(f(x))
    }
}

// [(num + 3) * 5 - 1] % 10 / 2
let calculate = add(3) >>> multiply(5) >>> subtract(1) >>> modulo(10) >>> divide(2)
let number = 10
let ret = calculate(number)
print(ret) //2

在上述代码中,我们对每个函数进行柯里化,使每个函数接受一个参数并返回一个新的函数,该新函数接受第二个参数并进行计算。这样,我们将每个函数转换为接受一个参数并返回一个结果的形式。

然后,我们使用函数组合符号 >>> 将这些柯里化的函数连接起来,形成一个流水线计算过程。它将按照从左到右的顺序依次调用每个函数,将前一个函数的结果作为参数传递给下一个函数。

最后,我们定义了一个输入参数 number,并将其作为参数传递给合并后的流水线计算函数 calculate,得到最终结果并存储在 ret 变量中。可以看到,给定相同的输入 10,获得相同的输出值 ret,即 2。

处理业务逻辑示例

诚然,优秀的编程范式最终得应用到实际的业务逻辑处理。下面是一个简单的例子,说明如何应用函数式编程思想来处理业务逻辑。

假设我们有一个餐厅的订单数据,每个订单包含菜品的名称、数量和价格。我们想要计算每个订单的总金额,并筛选出总金额大于某个阈值的订单。

struct Order {
    let itemName: String
    let quantity: Int
    let price: Double
}

let orders = [
    Order(itemName: "Pizza", quantity: 2, price: 12.99),
    Order(itemName: "Burger", quantity: 1, price: 8.99),
    Order(itemName: "Salad", quantity: 3, price: 6.99),
    Order(itemName: "Pasta", quantity: 2, price: 10.99)
]

let threshold = 20.0

// 计算每个订单的总金额
let totalAmounts = orders.map { order in
    order.price * Double(order.quantity)
}

// 筛选出总金额大于阈值的订单
let filteredOrders = zip(orders, totalAmounts)
    .filter { $0.1 > threshold }
    .map { $0.0 }

// 打印筛选后的订单
filteredOrders.forEach { order in
    print("Item: \(order.itemName), Quantity: \(order.quantity), Price: \(order.price)")
}

//Item: Pizza, Quantity: 2, Price: 12.99
//Item: Salad, Quantity: 3, Price: 6.99
//Item: Pasta, Quantity: 2, Price: 10.99

在上述代码中,我们首先定义了一个 Order 结构体来表示订单的数据结构。然后,我们创建了一个包含多个订单的数组 orders。

接下来,我们使用 map 函数对每个订单进行操作,计算每个订单的总金额,并将结果存储在 totalAmounts 数组中。在这里,我们使用了函数式编程的特性,将计算逻辑封装在一个匿名函数中,并使用 map 函数将其应用于每个订单,生成一个新的数组。

然后,我们使用 zip 函数将 orders 数组和 totalAmounts 数组进行合并,得到一个包含订单和对应总金额的元组数组。接着,我们使用 filter 函数过滤出总金额大于阈值的订单,并使用 map 函数提取出订单,得到一个新的订单数组 filteredOrders。

最后,我们使用 forEach 函数遍历 filteredOrders 数组,并打印出每个订单的信息。

通过使用函数式编程的方式,我们将业务逻辑分解为一系列的函数式操作,每个操作都是独立、可测试和可复用的。这种方式使得代码更加模块化、易于理解和维护,并且可以在需要时轻松地修改和扩展。

结束语

限于篇幅问题,这里不在对函子、适用函子及单子等展开讨论。然后,函数式编程是一种非常优秀的编程思想,它强调将计算过程视为函数的组合和转换,以及避免副作用和可变状态的影响。函数式编程鼓励使用纯函数、不可变数据和高阶函数等概念来构建可维护、可测试和可复用的代码。这里仅以 Swift为例,介绍一下函数式编程,希望可以起到抛砖引玉的作用。文中及代码示例难免有不当之处,敬请指正。

参考

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值