跳转至

第10章:高阶函数

一个高阶函数需要一个或多个函数作为参数。因此,你不是向函数发送正常值,而是向它发送另一个接受参数的函数。普通函数被称为一阶函数。这里有太多的functions了。

高阶函数的一个更普遍的定义是将它们标记为处理其他函数的函数,要么作为参数,要么作为返回类型。在本章中,你将从发送一个函数作为参数开始。然后你将转到让一个函数返回另一个函数。

你很快就会知道,高阶函数可以大大简化你的代码,使其更易读,更短,更容易重复使用。

一个简单的文本打印机

在深入研究什么是高阶函数或Swift如何让它们变得有趣之前,请考虑这个文本打印机的例子。

创建一个新的playground并添加以下内容:

class TextPrinter {
  var formatter: ParagraphFormatterProtocol
  init(formatter: ParagraphFormatterProtocol) {
    self.formatter = formatter
  }

  func printText(_ paragraphs: [String]) {
    for text in paragraphs {
      let formattedText = formatter.formatParagraph(text)
      print(formattedText)
    }
  }
}

protocol ParagraphFormatterProtocol {
  func formatParagraph(_ text: String) -> String
}

上面的代码是一个文本打印机。它所做的就是接收一个字符串数组,通过一个格式化器运行每个字符串,然后打印出格式化的值。TextPrinter并没有指定如何进行格式化操作。但它确实说明它需要一个符合ParagraphFormatterProtocol的对象,它将实现这一功能。

把这个新的类添加到你的playground

class SimpleFormatter: ParagraphFormatterProtocol {
  func formatParagraph(_ text: String) -> String {
    guard !text.isEmpty else { return text } // 1
    var formattedText =
      text.prefix(1).uppercased() + text.dropFirst() // 2
    if let lastCharacter = formattedText.last,
       !lastCharacter.isPunctuation {
      formattedText += "." // 3
    }
    return formattedText
  }
}

这个简单的格式化器符合规定的协议,其格式化功能做了以下工作:

  1. 确保提供的字符串不是空的。如果是空的,那么就没有什么需要格式化的,格式化器会按原样返回字符串。
  2. 将第一个字符大写
  3. 检查最后一个字符是否是标点符号。如果不是,就加上一个句号.

现在,在你的操场上使用这个新类:

let simpleFormatter = SimpleFormatter()
let textPrinter = TextPrinter(formatter: simpleFormatter)

let exampleParagraphs = [
  "basic text example",
  "Another text example!!",
  "one more text example"
]

textPrinter.printText(exampleParagraphs)

你创建了一个你刚刚定义的简单格式化器的实例,并用它来创建一个打印机的对象。上面的代码在控制台中的输出应该是:

Basic text example.
Another text example!!
One more text example.

一阶函数

这里的两个主要方法是第一-阶函数。ParagraphFormatterProtocol.formatParagraph(_:)TextPrinter.printText(_:)都取正常值。你还没有进入高阶的。

另一种执行格式化的方法是为字符串数组添加一个新的一阶函数。在你的playground末端添加这个新的扩展:

extension Array where Element == String {
  func printFormatted(formatter: ParagraphFormatterProtocol) {
    let textPrinter = TextPrinter(formatter: formatter)
    textPrinter.printText(self)
  }
}

这个扩展为Array添加了一个方法,只有当存储元素的类型是String时,才可以使用。所以这个方法不能用于IntAny的数组,只能用于String

使用该方法时,请调用:

exampleParagraphs.printFormatted(formatter: simpleFormatter)

这将打印出与之前相同的结果。

上面的实现一点都没有错。它们提供了所需的操作,并提供了一个明确的集成方法来提供不同的格式化操作。但是,每次都创建一个格式化器或重复使用同一个格式化器并不是特别Swifty。如果有一种方法可以将格式化操作本身作为一个参数来发送,而不是将其打包在一个对象中,会怎么样呢?

你的第一个高阶函数

创建一个新的playground,用一种新的方式来尝试这个方法。从前面的例子中提取你在SimpleFormatter中创建的格式化函数,并将其添加为一个函数:

func formatParagraph(_ text: String) -> String {
  guard !text.isEmpty else { return text }
  var formattedText = 
    text.prefix(1).uppercased() + text.dropFirst()
  if let lastCharacter = formattedText.last,
     !lastCharacter.isPunctuation {
    formattedText += "."
  }
  return formattedText
}

接下来,创建这个新的扩展:

extension Array where Element == String {
  func printFormatted(formatter: ((String) -> String)) {
    for string in self {
      let formattedString = formatter(string)
      print(formattedString)
    }
  }
}

这个扩展只对[String]有同样的想法。但是在它的方法中,注意参数的不同类型。((String) -> String)意味着这不是一个属性。相反,它是一个方法,接受一个String作为参数并返回一个String

在括号里,箭头前面的东西描述了参数,箭头后面的东西定义了返回类型。

你在扩展中添加的那个方法是一个高阶函数。为了让它工作,它需要另一个函数作为参数,而不是一个普通的属性。你可以像这样使用它:

let exampleParagraphs = [
  "basic text example",
  "Another text example!!",
  "one more text example"
]

exampleParagraphs.printFormatted(formatter: formatParagraph(_:))

你把实际的函数本身作为一个参数发送。另一种将函数作为参数传递的方式是这样的:

let theFunction = formatParagraph
exampleParagraphs.printFormatted(formatter: theFunction)

变量theFunction实际上是一个函数,而不是一个普通的属性。你可以看到它的类型是((String) -> String),并且可以把它描述为对一个函数的引用。

闭包

还有另一种形式是把函数作为参数传递过来,你很熟悉:

exampleParagraphs.printFormatted { text in
  guard !text.isEmpty else { return text }
  var formattedText = text.prefix(1).uppercased() + text.dropFirst()
  if let lastCharacter = formattedText.last,
     !lastCharacter.isPunctuation {
    formattedText += "."
  }
  return formattedText
}

在这个调用中,你发送了一个闭包,而不是一个函数。实际上,两者是一样的。这与直接发送函数引用是一样的。另一种方法是在闭包中调用函数。

exampleParagraphs.printFormatted { formatParagraph($0) }

当你想作为参数传递给高阶函数的函数与参数签名不匹配时,这种形式是完美的。一个例子是,如果你要调用或使用的函数有两个参数,但你会把其中一个作为常量发送,另一个则是来自主操作。那么你可以有如下的格式:

aHigherOrderFunction { someOperation($0, "a constant") }

所有这些例子的工作原理都是一样的,但它们各自都有一个微小的区别。把闭包想成是一个只为当前范围创建的函数。最后两个例子使用了闭包,但第一个例子直接给了一个现成的函数给printFormatted

你仍然可以直接使用函数作为参数,但是someOperation(::)的签名需要改变以符合aHigherOrderFunction()所期望的。这种变化被称为Currying,在本章的后半部分会介绍。

Swift标准库有很多这样的操作,都是使用这个概念。它们中的每一个都做了一些操作,但里面有一个小细节被忽略了。以sort函数为例。有许多排序算法。有些在小集合中表现较好,但在大集合中就不那么好了。然而,最后,它们都有一个两个项目之间的比较,定义了最终的排序是升序还是降序。在调用函数时,使用哪种算法以及如何使用并不重要,除了最后的结果是如何排序的。

一个返回函数的函数也被认为是一个高阶函数。返回类型是由箭头->后面的数据类型定义的。因此,你可以不定义类或结构体,而是有类似(()->())的东西,这是一个不接受任何参数也不返回任何东西的函数,或者你想要多少参数就有多少返回类型:((p1: Type1, p2: Type2, p3: Type3) -> ReturnType)

标准库中的高阶函数

Swift给你带来了一些高阶函数,它们提供了几种常见的操作,并有一种整洁的方式在你的代码中调用它们。这些函数中的一些是:

  • map
  • compactMap
  • flatMap
  • filter
  • reduce
  • sorted
  • split

现在你会更详细地了解它们,看看它们的使用实例。

map

Array.map(_:)对数组的所有元素进行操作,结果是一个相同大小的新数组。这是对元素进行迭代并将操作的新项添加到新数组中的一个更短的版本。在playground上试试这个例子:

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var newNumbers: [Int] = []

for number in numbers {
  newNumbers.append(number * number)
}

print(newNumbers)

这将创建一个新的数组,包含每个数字的平方。另一种使用map(_:)的方法是这样的:

let newNumbers2 = numbers.map { $0 * $0 }
print(newNumbers2)

这段代码比上一段要简单和简短得多。这是因为它使用了Swift提供的速记法和Swift闭包可能的所有代码缩减。但正如你在上面学到的,高阶函数可以接收一个函数而不是一个闭包。试试这个例子:

func squareOperation(value: Int) -> Int {
  print("Original Value is: \(value)")
  let newValue = value * value
  print("New Value is: \(newValue)")
  return newValue
}

这个函数对一个Int值进行平方,并打印出它的原始值和它的平方值。你可以使用map(_:),像这样把新的函数发送给它:

let newNumbers3 = numbers.map(squareOperation(value:))

compactMap

Array.compactMap(_:)和前面的一样,但是结果数组不需要和原始数组一样大。这个是过滤掉操作结果中的nil值。参数中的操作有一个可选的结果,但是map(_:)不允许这样。

这个方法的一个常见用法是将数组中的项目转换为不同的类型,知道不是所有的项目都能被转换,你将只得到被成功转换的元素。请看下面的例子:

func wordsToInt(_ str: String) -> Int? {
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut
  return formatter.number(from: str.lowercased()) as? Int
}

该函数接收一个字符串,并找出这个字符串是否是一个用字母表示的数字。例如,"one"这个词就是数字"1"。如果是,则该函数返回该数字。否则,它返回nil

wordsToInt("Three") // 3
wordsToInt("Four") // 4
wordsToInt("Five") // 5
wordsToInt("Hello") // nil

接下来,在你的playground的末尾添加这个函数:

func convertToInt(_ value: Any) -> Int? {
  if let value = value as? String {
    return wordsToInt(value)
  } else {
    return value as? Int
  }
}

这个函数接收任何数值并试图将其转换为整数。如果它是一个字符串,它会把它传递给你添加的前一个函数。否则,它试图将其转换为一个整数。

convertToInt("one") // 1
convertToInt(1.1) // nil
convertToInt(1) // 1

1.1转换失败,因为它是一个Double。现在,用这个样本数组试试吧:

let sampleArray: [Any] = [1, 2, 3.0, "Four", "Five", "sixx", 7.1, "Hello", "World", "!"]

let newArray = sampleArray.compactMap(convertToInt(_:)) // [1, 2, 4, 5]

到目前为止,它看起来不错。但是在做项目的时候,你了解到"3.0""7.1"这两个值应该被转换为整数。这个问题与compactMap(_:)一点关系都没有,但它在你整个项目中用于转换的方法中。把它更新为以下内容:

func convertToInt(_ value: Any) -> Int? {
  if let value = value as? String {
    return wordsToInt(value)
  } else if let value = value as? Double {
    return Int(value)
  } else {
    return value as? Int
  }
}

这使你的函数具有将双数转换为整数的能力。结果变成了[1, 2, 3, 4, 5, 7]

以后,你可以重构和改进convertToInt(_:),以转换更多的值,而不需要在你的项目中翻阅很多地方。这个具体的例子显示了一个更好的用法,即用一个函数来代替一个代码块或一个闭包作为高阶函数的参数。

flatMap

正如你在compactMap(_:)中看到的,你可以得到比原来数组中更少的结果。但是,如果你想要的操作将为每个项目提供一个数组呢?考虑到你想计算一个数组中每个数字的平方、立方和四次方。考虑一下下面的函数:

func calculatePowers(_ number: Int) -> [Int] {
  var results: [Int] = []
  var value = number
  for _ in 0...2 {
    value *= number
    results.append(value)
  }
  return results
}
calculatePowers(3) // [9, 27, 81]

现在,在一个整数的样本数组上使用这个函数:

let exampleList = [1, 2, 3, 4, 5]
let result = exampleList.map(calculatePowers(_:))
// [[1, 1, 1], [4, 8, 16], [9, 27, 81], [16, 64, 256], [25, 125, 625]]
result.count // 5

结果包含了所有你想要的值,但是它的大小仍然和原来的数组相同。唯一的区别是,现在的数组是一个数组的数组[[Int]]。对于原始集合中的每个元素,你为它创建了另一个集合。

如果你想把这些放在一个单一的、扁平的数组中,你可以做额外的步骤,把它们连接在一起:

let joinedResult = Array(result.joined())
// [1, 1, 1, 4, 8, 16, 9, 27, 81, 16, 64, 256, 25, 125, 625]

Swift标准库提供了一个更方便的方法,可以用更少的步骤来实现。不要使用map(_:),而要使用flatMap(_:)

let flatResult = exampleList.flatMap(calculatePowers(_:))
// [1, 1, 1, 4, 8, 16, 9, 27, 81, 16, 64, 256, 25, 125, 625]

这两种方式是等价的,并提供相同的结果。后者只是写起来更简单。

filter

filter(_:)是最简单的高阶函数之一。正如它的名字所暗示的,你想根据标准来过滤一个由许多项目组成的集合。如果该元素符合标准,就保留它。但如果不符合,就把它从列表中删除。

这个函数期望标准的形式是返回truefalse的函数。在列表中或不在列表中。

假设你想得到只包含四个字母的零到一百之间的数字词(英文单词中的数字,不是数字)的列表。首先,建立整个单词列表:

func intToWord(_ number: Int) -> String? {
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut
  return formatter.string(from: number as NSNumber)
}

let numbers: [Int] = Array(0...100)
let words = numbers.compactMap(intToWord(_:))
// ["zero", "one", "two", ....., "ninety-nine", "one hundred"]

接下来,创建一个函数来决定这个字符串是否应该留下来:

func shouldKeep(word: String) -> Bool {
  return word.count == 4
}

最后,过滤掉你的array

let filteredWords = words.filter(shouldKeep(word:))
// ["zero", "four", "five", "nine"]

如前所述,为了简单起见,你可以在这里使用一个闭包:

let filteredWords = words.filter { $0.count == 4 }

这是对的。但是,如果你的实际检查很复杂,并且在你的应用程序中到处使用,那么最好将其定义为一个函数而不是一个闭包。你的代码看起来会更好,而且如果需要改动的话,需要维护的地方也更少。

reduce

reduce(_:_:)是一个方便的方法,当你想合并一组元素的时候。常见的例子包括一组数字的加法或乘法。但这并不意味着它只对数字有效。你也可以在你自己的自定义类型上使用它。假设你有一个足球队的分数,你想把它们全部合并,以查看球队的总成绩。考虑一下这个Score类型:

struct Score {
  var wins = 0, draws = 0, losses = 0
  var goalsScored = 0, goalsReceived = 0

  init() {}

  init(goalsScored: Int, goalsReceived: Int) {
    self.goalsScored = goalsScored
    self.goalsReceived = goalsReceived

    if goalsScored == goalsReceived {
      draws = 1
    } else if goalsScored > goalsReceived {
      wins = 1
    } else {
      losses = 1
    }
  }
}

Score可以代表一场比赛或一组比赛。初始化器接收球队在一场比赛中的进球和得分,并根据这个分数设置winsdrawslosses值。

现在,建立一个单个比赛得分的数组:

var teamScores = [
  Score(goalsScored: 1, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 0),
  Score(goalsScored: 1, goalsReceived: 3),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 0),
  Score(goalsScored: 4, goalsReceived: 3)
]

接下来,创建一个函数来轻松地将两场比赛的分数相加。使用运算符重载是理想的做法:

extension Score {
  static func +(left: Score, right: Score) -> Score {
    var newScore = Score()

    newScore.wins = left.wins + right.wins
    newScore.losses = left.losses + right.losses
    newScore.draws = left.draws + right.draws
    newScore.goalsScored = 
      left.goalsScored + right.goalsScored
    newScore.goalsReceived = 
      left.goalsReceived + right.goalsReceived

    return newScore
  }
}

最后,使用reduce(_:_:)和你创建的新操作符方法,将这个团队的分数减少到一个Score对象中:

let firstSeasonScores = teamScores.reduce(Score(), +)
// Score(wins: 4, draws: 2, losses: 1, goalsScored: 13, goalsReceived: 9)

操作符的一个好处是,你不需要写出整个签名。操作符符号本身就足够了,没有必要像这样写:+(left:right:)

reduce(_:_:)中的第一个参数是你想加在上面的初始值。在这种情况下,除了现有的匹配,你没有其他东西,所以一个空的Score对象就足够了。但是,如果你想把第二个赛季的分数添加到本赛季的分数中,那么这个参数就相当方便了。

var secondSeasonMatches = [
  Score(goalsScored: 5, goalsReceived: 3),
  Score(goalsScored: 1, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 2),
  Score(goalsScored: 2, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 2),
  Score(goalsScored: 2, goalsReceived: 3)
]

let totalScores = secondSeasonMatches.reduce(firstSeasonScores, +)
// Score(wins: 7, draws: 4, losses: 3, goalsScored: 28, goalsReceived: 22)

你没有提供一个空的分数对象,而是提供了第一个赛季的分数作为初始值,而第二个赛季的所有比赛都被添加到它里面。

在这个例子中,你不可能为比赛分数和赛季分数创建单独的类型。reduce(_:_:)返回的类型与数组中元素的类型一致。这就是为什么用一个单一的类型来表示两者。

sorted

这可能是日常工作中最经常使用的功能之一。它为你对一个数组的元素进行排序。

存在多种排序算法,每种算法都有不同的复杂性和用途。在不同的情况下,有些比其他的更好。例如,插入式排序对小集合来说更好。然而,对于较大的集合,它的效率就不如其他的。你可以从我们的书《Swift中的数据结构和算法》中了解不同的排序算法--它解释了算法之间的差异。

Swift标准库中,排序实现多年来一直在变化。在Swift 5.0中,它的实现使用"Timsort",这是一种稳定的排序算法。这里,稳定的意思是,如果两个元素有相同的排序分数,那么它们在原始集合中出现的顺序在最终的排序结果中会被保持。

为了了解排序的实际效果,请在一个从010的数字的英文单词数组上尝试一下。

let words = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]

let stringOrderedWords = words.sorted()
// ["eight", "five", "four", "nine", "one", "seven", "six", "ten", "three", "two", "zero"]

排序工作按字母升序进行,从E开始,以Z结束。当数组中的项目具有可比性时,默认排序为升序,相当于words.sorted(<)

sorted(_:)期待一个表达式,它需要两个参数并定义第一个参数是否应该出现在第二个参数之前。

使用与之前定义的相同的Score,创建一个新的方法来检查所提供的两个匹配分数是否排序:

func areMatchesSorted(first: Score, second: Score) -> Bool {
  if first.wins != second.wins { // 1
    return first.wins > second.wins
  } else if first.draws != second.draws { // 2
    return first.draws > second.draws
  } else { // 3
    let firstDifference = first.goalsScored - first.goalsReceived
    let secondDifference = second.goalsScored - second.goalsReceived

    if firstDifference == secondDifference {
      return first.goalsScored > second.goalsScored
    } else {
      return firstDifference > secondDifference
    }
  }
}
  1. 如果只有一个人是赢家,这种方法会优先考虑赢家的比赛。
  2. 如果没有获胜的比赛,则优先考虑平局的比赛。
  3. 如果两场比赛都是胜、平或负,则得到进球数和得球数的差值。如果两场差额相同,则退回进球多的比赛或差额大的比赛。

用这个函数对一组比赛进行排序会得到以下结果:

var teamScores = [
  Score(goalsScored: 1, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 0),
  Score(goalsScored: 1, goalsReceived: 3),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 0),
  Score(goalsScored: 4, goalsReceived: 3)
]

let sortedMatches = teamScores.sorted(by: areMatchesSorted(first:second:))
//[Score(wins: 1, draws: 0, losses: 0, goalsScored: 3, goalsReceived: 0),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 4, goalsReceived: 3),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 2, goalsReceived: 1),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 1, goalsReceived: 0),
// Score(wins: 0, draws: 1, losses: 0, goalsScored: 2, goalsReceived: 2),
// Score(wins: 0, draws: 1, losses: 0, goalsScored: 0, goalsReceived: 0),
// Score(wins: 0, draws: 0, losses: 1, goalsScored: 1, goalsReceived: 3)]

最后的结果是得分差距最大的比赛,其次是得分最高的平局,最后是差距最小的输球,然后是较大的差距。

排序不是专门针对数字或可比较的类型。你可以在任何类型上实现comparable并使用<>操作符。或者你可以有你自己的比较方法或方法。

正如前面解释的,高阶函数可以为你做很多事情。它们可以为你简化一个复杂的功能,同时保留其整体操作的一个微小的、简单的部分,在调用时提供。

到目前为止,你已经深入了解了高阶函数的一种形式:期望其部分功能作为参数的函数。这是最简单的部分。在开始下一节之前,先休息一下,拿杯咖啡(建议喝浓咖啡),做任何你需要的事情,让你的头脑休息一下。这将有所帮助。

函数是一种返回类型

返回其他函数的函数也是高阶函数。这可能不是你所习惯的。但它可以大大增强你的代码,使其变得简单,尽管使用的是间接的方法,一开始看起来很复杂。请确保你已经理清了思路,所以不会觉得太复杂。

Currying

早些时候,你了解到将一个函数直接发送到一个高阶函数需要签名的匹配。因此,下面的例子最好通过一个闭包来完成,因为签名不匹配。

aHigherOrderFunction { someOperation($0, "a constant") }

通常情况下,这很好。但是去除闭包的使用并直接发送一个函数也不是不可能。

要删除闭包,你首先需要稍微改变someOperation(_:_:)的签名。新的用法将是这样的:

aHigherOrderFunction { curried_SomeOperation("a constant")($0) }

而如果让内部函数作为一个参数传递,而不是闭包:

aHigherOrderFunction(curried_SomeOperation("a constant"))

现在,退一步观察一下curried_SomeOperation。有两个主要变化:

  1. 现在参数被分开发送,每个都在自己的括号里。
  2. 参数被调换了。常数首先被传递。

Currying是将一个需要多个参数的函数分解为一连串的函数,其中每一个都需要一个参数。为了看到这个动作,你将通过实现上述例子进行练习,从someOperation(_:_:)curried_SomeOperation(_:)

创建一个新的操场页面并添加以下内容:

func aHigherOrderFunction(_ operation: (Int) -> ()) {
  let numbers = 1...10
  numbers.forEach(operation)
}

func someOperation(_ p1: Int, _ p2: String) {
  print("number is: \(p1), and String is: \(p2)")
}

aHigherOrderFunction { someOperation($0, "a constant") }

现在,抽象的东西已经变得更加具体,从curried_SomeOperation的签名开始。首先,把它分解。创建一个链,每次取一个参数,而不是取两个参数。以后再担心两个参数的重新排序问题。

func curried_SomeOperation(_ p1: Int) -> ((String) -> ())

第一步已经完成。好吧,已经完成了,但它仍然可以使用清理。签名看起来很简单。该函数接收一个参数,并返回另一个签名为(String) -> ()的函数。

现在,是真正的body的时候了:

func curried_SomeOperation(_ p1: Int) -> (String) -> () {
  return { str in
    print("number is: \(p1), and String is: \(str)")
  }
}

本体返回一个闭包,该闭包接受一个String类型的参数。这个闭包是(String) -> (),在它里面,发送到原始函数的p1被捕获并使用。

Note

为了避免保留循环,你需要注意捕获列表,如果属性是引用类型而不是值类型。

到目前为止,你并没有破坏任何东西。试试吧:

aHigherOrderFunction { curried_SomeOperation($0)("a constant") }

输出是:

number is: 1, and String is: a constant
.
.
.
number is: 10, and String is: a constant

像以前一样,你使用了一个闭包,但是仍然需要它。为了消除闭包的使用,你需要对参数进行重新排序。但是在这样做之前,你应该知道为什么。

aHigherOrderFunction(_:)期望的类型是(Int) -> ()。你的函数在提供了Int参数后并不返回()。相反,它返回(String) -> ()

如果你改变参数的顺序,你就会有(Int) -> ()的签名,你需要避免使用闭包。把curried_SomeOperation改成这样:

func curried_SomeOperation(_ str: String) -> (Int) -> () {
  return { p1 in
    print("number is: \(p1), and String is: \(str)")
  }
}

现在字符串首先被期望,这个闭包将Int作为一个参数。这个闭包现在与aHigherOrderFunction(:)的签名预期相匹配。现在的用法是:

aHigherOrderFunction { curried_SomeOperation("a constant")($0) }

你可以安全地减少它,消除闭包:

aHigherOrderFunction(curried_SomeOperation("a constant"))

你现在明白了什么是咖喱,为什么翻转参数是有用的。但是,如果有一种方法可以制作一个通用的curryflip,这样你就不需要为你遇到的每个方法创建一个咖喱/翻转的版本,那会怎么样?

很高兴你这么问!

一个通用的咖喱函数

使用一个原始函数的签名originalMethod(A, B) -> C,你想把它转化为。(a) -> (b) -> c

注意,你提到了一个返回类型(C),但是在someOperation中,没有返回类型。技术上讲,返回类型是Void

你的通用双参数咖喱方法的签名是:

func curry<A, B, C>(
  _ originalMethod: (A, B) -> C
) -> (A) -> (B) -> C

在进行实现之前,这个通用方法所做的事情与你在前面的例子中所做的事情有一个重要的区别。在这里,你正在生成你自己在例子中所做的事情,这意味着这将改变原始函数的签名。尽管这可能是显而易见的,但还是值得指出,因为它引入了与之前的咖喱例子的两个关键区别。

键入完整的咖喱实现,如下所示:

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

第一个区别是有一个额外的闭包层次。以前,你返回的是闭包(Int) -> ()。在这里,你返回一个更长的链,因为第一组参数是你要转换的实际函数。

第二个区别是明确的关键字@escaping。这一点是需要的,因为你传递给它的函数将在咖喱函数本身完成后执行。

由于上述两个原因,curry是一个高阶函数。它接受一个函数作为参数,同时也返回一个函数。

试着使用这个函数并与原来的someOperation进行比较:

someOperation(1, "number one")
curry(someOperation)(1)("number one")

在操场上使用新的方法会得到这个预期的结果,证明原来的函数和被诅咒的函数都在按预期工作:

number is: 1, and String is: number one
number is: 1, and String is: number one

curry(someOperation)的返回类型是(Int) -> (String) -> ()。这与你之前做的第一步是一样的。接下来,你需要将参数翻转为(String) -> (Int) -> ()

通用的参数翻转

翻转不会像currying那样令人困惑,因为你不需要对签名做任何大幅度的修改。添加以下函数:

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

flip(curry(someOperation))的类型是(String)->(Int)->(),这与你之前的例子中最终的curried_SomeOperation完全相同。在你的操场上试试这个:

aHigherOrderFunction(flip(curry(someOperation))("a constant"))

你不需要再写任何代码来调整你现有的函数,使其直接传递给高阶函数。

对于翻转的具体情况,你可能觉得你可以设计你的API来完全避免需要它。但Swift包含一些高阶函数,使翻转成为必要。

Swift生成的类方法

对于你创建的每个方法或实例函数,Swift都会为这个方法创建一个类高阶函数。在一个新的playground页面中,添加这个扩展:

extension Int {
  func word() -> String? {
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut
    return formatter.string(from: self as NSNumber)
  }
}

Int上的这个扩展产生了该数字的词:

1.word() // one
10.word() // ten
36.word() // thirty-six

默认情况下,Swift会为你的这个方法生成一个高阶函数:

Int.word // (Int) -> () -> Optional<String>

要使用它与上面使用的三个数字相同,你的代码将看起来像这样:

Int.word(1)() // one
Int.word(10)() // ten
Int.word(36)() // thirty-six

如果你有一个整数数组,并想把它们映射到它们的单词等价物,你可以创建一个闭包并在每个对象上调用word(),或者重组Int.word以符合map(_:)的签名要求。

在同一个操场上,创建一个新版本的flip

func flip<A, C>(
  _ originalMethod: @escaping (A) -> () -> C
) -> () -> (A) -> C {
  return { { a in originalMethod(a)() } }
}

这个重载将不接受任何参数的部分移到开头,允许在最后发送参数。

flip(Int.word)()(1) // one

你可以通过创建一个新的变量,携带函数flip(Int.word)()来避免重复flip

var flippedWord = flip(Int.word)()

你可以直接使用你的新属性,就像它是一个函数一样:

[1, 2, 3, 4, 5].map(flippedWord)
// ["one", "two", "three", "four", "five"]

在某些场合,额外的括号()可能会带来不便。你可能想把任何生成的高阶函数转换为一个不需要任何参数的方法/实例函数,如果它是一个标准的类函数,就可以使用它。创建这个新函数:

func reduce<A, C>(
  _ originalMethod: @escaping (A) -> () -> C
) -> (A) -> C {
  return { a in originalMethod(a)() }
}

var reducedWord = reduce(Int.word)

reducedWordflippedWord是相同的,都是(Int) -> String?。但是如果你注意一下后者的声明,你会发现已经加了括号,好像它包含了flip的外层闭包的结果。但是reduce本来就没有外部闭合。

合并高阶函数

你可以对高阶函数做一个有趣的技巧,就是合并它们。通常情况下,如果你想把两个或更多的函数连接起来,你会创建一个同时做这两件事的函数并使用这个新函数。

考虑一下下面的扩展:

extension Int {
  func word() -> String? {
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut
    return formatter.string(from: self as NSNumber)
  }

  func squared() -> Int {
    return self * self
  }
}

如果你想用一个函数来做这两件事,它应该是这样的:

func squareAndWord() -> String? {
  self.squared().word()
}

这绝对不是错的。但你可能有许多不只是两个你想捆绑的功能。有一个很好的方法可以做到这一点。添加这个通用函数合并:

func mergeFunctions<A, B, C>(
  _ f: @escaping (A) -> () -> B,
  _ g: @escaping (B) -> () -> C
) -> (A) -> C {
  return { a in
    let fValue = f(a)()
    return g(fValue)()
  }
}

这个函数是为Swift从接受一个参数的方法中生成的高阶函数量身定做的。注意到参数和返回类型之间的数据类型的关系是很重要的。第一个函数的返回类型与第二个函数的参数相匹配。如果它们不匹配,那么把它们连锁起来就没有意义了。试一试吧:

var mergedFunctions = mergeFunctions(Int.squared, Int.word)
mergedFunctions(2) // four

你可能会想,难道没有一个更好的方法来使用它吗?这几乎看起来像你添加了两个函数。用同样的方法但用运算符重载怎么样?

在你的操场上添加以下内容:

func +<A, B, C>(
  left: @escaping (A) -> () -> B,
  right: @escaping (B) -> () -> C
) -> (A) -> C {
  return { a in
    let leftValue = left(a)()
    return right(leftValue)()
  }
}

现在,有趣的部分。试试吧:

var addedFunctions = Int.squared + Int.word
addedFunctions(2) // four
(Int.squared + Int.word)(2) // four

这被称为函数组合。你用两个较小的函数合成了一个函数。这是一个更大的话题,但一旦你理解了如何玩弄高阶函数,函数组合就会变得更加清晰。

你可以自由地定义你想要的运算符。但要知道,一旦你的代码中开始有太多的运算符,它在别人看来就会很陌生。你的专有约定会让其他的眼睛感到复杂。

关键点

  • 高阶函数是一个处理其他函数的函数,可以是参数,也可以是返回类型。
  • Swift允许在高阶函数中使用闭包或函数签名,只要参数数量和返回类型与原始高阶函数声明相同。
  • 如果操作复杂或在代码中重复,使用函数签名而不是闭包可以简化你的代码。
  • mapcompactMapflatMapfilterreducesortedsplit都是标准库中高阶函数的例子。
  • 高阶函数也描述了将函数作为返回类型的函数。
  • 函数卷曲是指将一个需要多个参数的函数分解成一连串的函数,每个函数只需要一个参数。
  • 咖喱和参数翻转是改变一个函数的签名以适应高阶函数的方法。
  • 每个实例方法都可以通过其包含的类型作为一个高阶函数使用。
  • 函数组合是指你将高阶函数合并以创建更大的函数。
  • 你可以使用运算符重载来为高阶函数创建一个添加函数,使函数组合更容易。

接下来去哪?

在标准库中还有其他高阶函数,如split(_:), contains(_:), removeAll(_:)forEach(_:)。本章的目的不是要解释库中的所有函数,而是要说明它们如何使你的代码更短、更简单。

你可以在《数据结构&Swift中的算法》一书中阅读不同的算法,或者从https://github.com/raywenderlich/swift-algorithm-club上的swift-algorithms-club阅读,该书就是基于此。