FunctionKit

Swift 4.1 Build Status MIT @pangburnout

一个为函数式类型和操作设计的框架,旨在自然地融入 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 name = nameTextField.text
let withoutExtraWhitespace = removeExtraWhitespace(name)
let withoutEmojis = removeWeirdUnicodeCharacters(withoutExtraWhitespace)
let properlyCapitalized = capitalizeProperly(withoutEmojis)

这看起来像是助手函数的工作。让我们快速编写一个

func sanitize(name: String) -> String {
    let withoutExtraWhitespace = removeExtraWhitespace(name)
    let withoutEmojis = removeWeirdUnicodeCharacters(withoutExtraWhitespace)
    let properlyCapitalized = capitalizeProperly(withoutEmojis)
    return properlyCapitalized
}

您甚至可以选择用一行代码编写它

func sanitize(name: String) -> String {
    return properlyCapitalized(withoutEmojis(removeExtraWhitespace(name)))
}

不幸的是,当我们对同一个输入调用更多函数时,我们似乎会陷入以下两个问题之一,具体取决于我们的方法

  1. 过多的局部变量来分隔步骤。
  2. 括号式的混乱。

虽然优化器应确保两种情况下的功能相同,但选项 1 感觉不必要地冗长,而选项 2 严重阻碍了代码从左到右的可读性。

FunctionKit 允许我们使用组合以简单、清晰和声明式的方式重写此函数

let sanitize = Function.pipeline(removeExtraWhitespace, removeWeirdUnicodeCharacters, capitalizeProperly)
let sanitizedNames = names.map(sanitize)

这里发生了什么?

在您生命中的某个时候的数学课程中,您可能被介绍过函数组合的概念

(g ∘ f)(x) = g(f(x))

基本思想是通过获取一个函数的输出并将其用作另一个函数的输入来创建新函数。

我们使用 Function 类型来包装 Swift 函数,并为其提供强大的新功能——双关语。静态 pipeline 方法将每个函数的输出向前传递到下一个函数。piped(into:) 实例方法对单个函数执行相同的操作。

我们也可以使用组合来转换类型

let sanitizedCount = sanitize.piped(into: { $0.count })
let sanitizedNameCounts = names.map(sanitizedCount) // [Int]

通过将函数用作可组合的、可转换的单元,我们增强了模块化和表达性。 FunctionKit 提供了许多工具来简化函数式类型的使用。

目标

重要提示: 将 Swift 变成纯函数式编程语言不是 FunctionKit 的目标。 FunctionKit 以一种自然地融入语言的方式拥抱并增强了 Swift 的函数式功能。

FunctionKit 倾向于迭代语言的方法点语法,而不是自由函数或运算符。有关 Swift 中函数式编程构造的更传统应用,请参阅 OverturePreludeSwiftz

用法

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() 方法,我们可以取消柯里化未应用的 method reference

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

ConsumerProvider

Consumer 类型定义为

typealias Consumer<Input> = Function<Input, Void>

Consumer 类型描述了不产生输出的函数,例如修改状态或记录数据的函数。 Consumer 实例可以使用 then(_:) 方法进行链式调用

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 实例对于比较同一类型的两个值很有用,尤其是在排序时。它们可以通过多种方式创建

创建后,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 建模,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 Manager

将以下行添加到您的 Package.swift 文件

.package(url: "https://github.com/mpangburn/FunctionKit", from: "0.1.0")

参考

许可证

FunctionKit 在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE