FunctionKit
一个旨在自然集成到 Swift 中的功能性类型和操作框架。
目录
背景
作为一门一等函数语言,Swift 支持将函数用作值。这意味着函数可以被存储在变量中,并作为参数传递给其他函数。
当您处理序列时,可能遇到过 Swift 的一些功能性 API。
let numbers = 1...5
let incrementedNumbers = numbers.map { $0 + 1 } // [2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 } // [2, 4]
有时对序列执行多个操作是很有必要的
let names = ["LAUREN ", "michael", "JiM", " Alison"]
let sanitizedNames = names
.map(removeExtraWhitespace)
.map(capitalizeProperly)
这看起来不错,但我们也引入了效率低下的问题:在映射数组两次时,我们无意中创建了一个中间数组。这里有一对潜在的解决方案
- 在单个 map 中一起进行函数调用。
- 使用
lazy
属性。
选项 1 很快就会变成括号噩梦,而选项 2 通常会因为需要调用 Array
初始化器而影响可读性。函数组合可以优雅地解决这个问题
let sanitize = Function(removeExtraWhitespace).piped(into: capitalizeProperly)
let sanitizedNames = names.map(sanitize)
这里发生了什么?
虽然 Swift 函数功能强大,但我们不幸不能写出
extension <A, B> (A) -> B {
// implement a method for all functions of form (A) -> B
}
取而代之,我们使用《Function
》类型来包装 Swift 函数,并为其提供强大的新功能——并不是字面上的意思。`piped(into:)
` 方法创建一个新的函数,它接受 `removeExtraWhitespace
` 的输出并将其用作 `capitalizeProperly
` 的输入。
我们还可以使用组合来转换类型
let sanitizedCount = sanitize.piped(into: { $0.count })
let sanitizeNameCounts = names.map(sanitizedCount) // [Int]
通过将函数作为可组合、可转换的单元使用,我们增强了模块化和可表达性。FunctionKit 提供了多种工具,使使用函数类型变得简单。
目标
- 清晰度 —— FunctionKit 致力于在使用时达到清晰度。在适当的情况下,方法名称遵循术语,但不会回避对意图的明确描述。
- 直观性 —— 通过将 Swift 函数包裹在
Function
对象中,它获得了访问功能操作如组合和柯里化的权限,这些操作通过清晰且易于发现的单例方法进行。 - 简单性 —— 接受其他
Function
对象作为输入的 `Function
` 方法具有重载以支持直接使用 Swift 函数,并且原生的 Swift 方法(如 `Sequence.map
`)也有重载,以接受Function
对象。结果是更简单、更直观、更清晰的 API。
重要: FunctionKit 的一个 非目标 是将 Swift 转变为纯函数编程语言。FunctionKit 采取并增强了 Swift 的功能能力,使其自然地融入到语言中。
FunctionKit 倾向于迭代语言的点方法语法而不是自由函数或运算符。对于在 Swift 中更传统地应用functional programming 构造,请参阅 Overture、Prelude 和 Swiftz。
用法
FunctionKit 的主要单元是 Function
类型,该类型包装了 Swift 函数。使用它的初始化器创建一个 Function
let makeRandom = Function(arc4random_uniform) // Function<UInt32, UInt32>
let stringFromData = Function(String.init(data:encoding:)) // Function<(Data, String.Encoding), String?>
let increment = Function { (x: Int) in x + 1 } // Function<Int, Int>
或者,使用本节随后描述的其中一个静态方法,通过组合几个 Swift 函数初始化一个 Function
。
要调用 Function
,使用 apply(_:)
方法。
let random = makeRandom.apply(100) // 42, perhaps
let parsed = stringFromData.apply(Data(), .utf8) // Optional<String>.some("")
let incremented = increment.apply(6) // 7
一旦被 `Function
` 包装,就可以访问强大的功能 API。
函数操作
通过 Function
类型支持以下函数操作
正向组合
正向组合是通过将一个函数的输出作为另一个函数的输入来创建一个新的函数的过程。正向组合的过程可以描述为
pipe
(A) -> B
(B) -> C
=>
(A) -> C
要使用正向组合函数,请使用 piped(into:)
方法
let sanitize = Function(removeExtraWhitespace).piped(into: capitalizeProperly) // Function<String, String>
可以使用静态 pipeline
方法对一系列函数进行正向组合
let sanitizedCount = Function.pipeline(removeExtraWhitespace, capitalizeProperly, { $0.count }) // Function<String, Int>
连接
连接是将具有相同输入和输出类型的函数进行正向组合的过程。连接的过程可以描述为
concatenate
(A) -> A
(A) -> A
=>
(A) -> A
虽然这种功能可以通过正常的正向组合完全提供,但是在连接的调用点立即可以清楚地看到类型保持不变。因此,连接是一种非常重要的操作,可以提高类型安全和清晰意图。
要连接函数,请使用 concatenated(with:)
方法
let sanitize = Function(removeExtraWhitespace).concatenated(with: capitalizeProperly) // Function<String, String>
可以使用静态 concatenation
方法连接一系列函数
let sanitize = Function.concatenation(removeExtraWhitespace, removeWeirdUnicodeCharacters, capitalizeProperly) // Function<String, String>
可选链式调用
链式调用是将返回 Optional
值的函数进行正向组合的过程。如果链中的任何函数返回 nil
,则整个函数返回 nil
。链式调用的过程可以描述为
chain
(A) -> B?
(B) -> C?
=>
(A) -> C?
要链式调用函数,请使用 chained(with:)
方法
let urlStringHost = Function(URL.init(string:)).chained(with: { $0.host }) // Function<String, String?>
可以使用静态 chain
方法链式调用一系列返回 Optional
的函数
let urlStringHostFirstCharacter = Function.chain(URL.init(string:), { $0.host }, { $0.first }) // Function<String, Character?>
反向组合
反向组合是通过将一个函数应用于另一个函数的输出来创建一个新的函数的过程。反向组合的过程可以描述为
compose
(B) -> C
(A) -> B
=>
(A) -> C
虽然在参数顺序相反时,前向组合提供了完整的功能,但有时使用后向组合来编写代码更具表达性。可以将后向组合视为将一个类型的函数“提升”到另一个类型的函数。
要后向组合函数,请使用 composed(with:)
方法
let sanitize = Function(capitalizeProperly).composed(with: removeExtraWhitespace) // Function<String, String>
可以使用静态 composition
方法来后向组合一系列函数
let sanitizedCount = Function.composition({ $0.count }, removeExtraWhitespace, capitalizeProperly) // Function<String, Int>
柯里化
柯里化是将接受元组输入参数的函数分割成一系列函数的过程。将两个参数的函数柯里化的过程可以描述为
curry
(A, B) -> C
=>
(A) -> (B) -> C
柯里化函数接受一个参数并返回一个函数。
柯里化对于部分应用函数非常有用,即提供一个参数值来生成一个接收较少参数的函数。
例如,使用 curried()
方法,我们可以柯里化和部分应用整数加法
// CurriedTwoArgumentFunction<A, B, C> is a typealias for Function<A, Function<B, C>>.
let curriedAdd: CurriedTwoArgumentFunction<Int, Int, Int> = Function(+).curried()
let addToFive = curriedAdd.apply(5) // Function<Int, Int>
addToFive.apply(3) // 8
addToFive.apply(20) // 25
在部分应用函数时,可以使用 flippingFirstTwoArguments()
方法翻转参数的顺序,这可能有所帮助。
// In describing the steps below, standard Swift function notation will be used over `Function` type notation
// to demonstrate the operations performed more clearly.
let utf8StringFromData =
Function(String.init(data:encoding:)) // (Data, String.Encoding) -> String?
.curried() // (Data) -> (String.Encoding) -> String?
.flippingFirstTwoArguments() // (String.Encoding) -> (Data) -> String?
.apply(.utf8) // (Data) -> String?
虽然柯里化函数通常提供最大灵活性,但有时将柯里化函数“非柯里化”也可能很有用。非柯里化两个参数的过程可以描述为
uncurry
(A) -> (B) -> C
=>
(A, B) -> C
例如,使用 uncurried()
方法,我们可以非柯里化一个未应用的方法引用
let stringHasPrefix = String.hasPrefix // (String) -> (String) -> Bool
let uncurriedHasPrefix = Function(stringHasPrefix).uncurried() // Function<(String, String), Bool>
uncurriedHasPrefix.apply("function", "func") // true
注意: 如果实现了SE-0042,则未应用方法引用的行为可能会发生变化。
KeyPath 支持特性
静态 get
方法接受一个 KeyPath<Root, Value>
,并返回一个从根中提取值的函数。
// The following two functions have the same effect:
let getStringCount1: Function<String, Int> = .init { $0.count }
let getStringCount2 = Function.get(\String.count)
静态 update
方法接受一个 WritableKeyPath<Root, Value>
,并返回一个setter函数,它将更新类型属性的更改传播到该类型的实例。
struct Person {
var name: String
}
let updateName = Function.update(\Person.name) // Function<Function<String, String>, Function<Person, Person>>
let lowercaseName = updateName.apply { $0.lowercased() } // Function<Person, Person>
let MICHAEL = Person(name: "MICHAEL")
let michael = lowercaseName.apply(MICHAEL)
// michael.name == "michael"
警告: 使用由 update
方法产生的函数与可变引用类型可能产生意外行为。
特设函数类型
某些函数类型因其常用任务(如过滤和排序)而在实际使用中尤为常见。FunctionKit 为以下类型提供了额外的 API
Consumer
和 Provider
Consumer
类型定义为
typealias Consumer<Input> = Function<Input, Void>
Consumer
类型描述了一个不产生输出的函数,如下面的修改状态或记录数据的函数。可以使用 then(_:)
方法将 Consumer
实例连接在一起
let handleError = Consumer<Error>
.init(presentError)
.then(analyticsManager.logError)
Consumer
类型适合与可变引用类型一起使用
let configureLabel = Consumer<UILabel>
.init(stylizeFont)
.then { $0.numberOfLines = 0 }
.then(view.addSubview)
configureLabel.apply(detailLabel)
注意:Consumer
并非为建模会修改值类型的 inout
函数而设计的,为此存在另一个专门的类。
Provider
类型的定义如下
typealias Provider<Output> = Function<Void, Output>
Provider
类型描述了可以不接收输入就产生输出的工厂方法。可以通过 make()
方法调用它们
let timestampProvider = Provider(Date.init)
let now = timestampProvider.make()
let idProvider = Provider(IdentifierFactory.makeId)
let id = idProvider.make()
Predicate
Predicate
类型的定义如下
typealias Predicate<Input> = Function<Input, Bool>
Predicate
实例用于验证输入和过滤。可以通过 test(_:)
方法调用,通过 negated()
方法或前缀 !
运算符取反,以及使用中缀 &&
和 ||
运算符进行逻辑组合。
由于某些谓词非常常见,还提供了额外的静态函数如 isEqualTo(_:)
、isLessThan(_:)
和 isInRange(_:)
。
let hasValidLength: Predicate<String> = Function
.get(\String.count)
.piped(into: .isInRange(4...12))
let usesValidCharacters = Predicate<String>
.init { $0.contains(where: invalidCharacters.contains) }
.negated()
let isValidUsername = hasValidLength && usesValidCharacters
也可以使用静态 all(of:)
和 any(of:)
方法创建 Predicate
实例
let isOddPositiveMultipleOfThree: Predicate<Int> =
.all(of:
{ $0 % 2 != 0 },
{ $0 > 0 },
{ $0 % 3 == 0 }
)
(-15...15).filter(isOddPositiveMultipleOfThree) // [3, 9, 15]
Comparator
Comparator
类型的定义如下
typealias Comparator<T> = Function<(T, T), Foundation.ComparisonResult>
Comparator
实例用于比较相同的两个值,尤其是在排序方面非常有用。它们可以通过各种方式创建
- 对
Comparable
类型的Comparator
可以通过静态naturalOrder()
和reverseOrder()
方法创建。 - 基于类型之一的
Comparable
属性,可以静态使用comparing(by:)
方法创建类型的Comparator
。 - 基于类型之一的
Optional
Comparable
属性,可以静态使用nilValuesFirst(by:)
和nilValuesLast(by:)
方法创建类型的Comparator
。
创建后,可以
- 使用
thenComparing(by:)
方法进行排序。 - 使用
reversed()
方法取反。 - 使用
lifting(with:)
方法提升到其他类型的Comparator
。
struct User {
let id: Int
let signupDate: Date
let email: String?
}
// Compares `User` instances, where
// - emails are compared lexicographically, with `nil` values coming after non-`nil` values
// - ties (i.e. two emails are the same, or both are `nil`) are broken by comparing the users' ids, with the lower id coming first.
let userEmailThenId = Comparator<User>
.nilValuesLast(by: { $0.email })
.thenComparing(by: { $0.id })
let sortedUsers = users.sorted(by: userEmailThenId)
可以通过使用静态 sequence
方法从该类型的一系列 Comparator
实例创建类型的 Comparator
。
// Compares `User` instances, where
// - users who signed up earlier come first
// - if users signed up at the exact same time, their emails are compared lexicographically
// - if users' emails are identical or both `nil`, the user with the lower id comes first
let userSignupDateThenEmailThenId: Comparator<User> =
.sequence(
.comparing(by: { $0.signupDate }),
.nilValuesLast(by: { $0.email }),
.comparing(by: { $0.id })
)
Inout 函数
类型为 (inout A) -> Void
的函数可以使用 InoutFunction
模拟,它是一个与 Function
不同的独立类型,能够提供连接 inout 函数的功能。
使用 toInout()
方法可以将 Function<A, A>
转换为 InoutFunction<A>
,反过来使用 withoutInout()
方法。
let increment = Function { (x: Int) in x + 1 } // Function<Int, Int>
let inoutIncrement = increment.toInout() // InoutFunction<Int>
var x = 1
inoutIncrement.apply(&x) // x == 2
inoutIncrement.apply(&x) // x == 3
抛出异常的函数
在未来的更新中将会支持抛出异常的函数——请持续关注更新!
安装
Carthage
将以下行添加到您的 Cartfile 中
github "mpangburn/FunctionKit" ~> 0.1.0
CocoaPods
将以下行添加到您的 Podfile 中
pod 'FunctionKit', '~> 0.1.0'
Swift 包管理器
将以下行添加到您的 Package.swift 文件中
.package(url: "https://github.com/mpangburn/FunctionKit", from: "0.1.0")
参考文献
许可
FunctionKit是在MIT许可下发布的。有关详细信息,请参阅LICENSE。