序言

Actions Status

Prelude 是一个由 Wayfair 开发的库,用于 Swift 中的函数式编程。我们在 我们的应用 中使用这个库,以函数式风格构建功能。

安装

可以通过 CarthageSwift Package Manager 在您的项目中安装 PreludeCocoaPods 的支持即将推出

Carthage 自定义钩子

如果您通过 Carthage 集成 Prelude,并且需要调整构建设置(例如,您可能希望将库构建为静态框架,以提高应用启动速度),我们包含了一个对特殊自定义文件的引用,这可能对您有所帮助。

如果还有其他我们可以做的事情来支持将 Prelude 集成到您的项目中,请提出 issue!

目录

继续阅读了解详情,或直接前往 playground

变更追踪

1. 使用 Changeable 记录值何时发生更改

Changeable 是一种包装类型,允许您将 hasChanged 标志与数据片段捆绑在一起。如果您需要告知某人一个值是否已更改,请使用它

let wasUpdated = Changeable(hasChanged: true, value: "foo")

2. 从函数返回 Changeable 以表示空操作

Changeable 用作函数的返回值时,它会变得更有用。您可以轻松地向调用者传达他们对您的调用是否为空操作

struct User { var firstName: String }

/// return a new `User` value with the first name changed to "hamburgers".
/// If the first name is already "hamburgers", do nothing.
func updateFirstName(_ user: User) -> Changeable<User> {
    if user.firstName == "hamburgers" {
        return Changeable(hasChanged: false, value: user)
    } else {
        let newUser = User(firstName: "hamburgers")
        return Changeable(hasChanged: true, value: newUser)
    }
}

let myUser = User(firstName: "hamburgers")
updateFirstName(myUser) // .hasChanged => false

3. 使用 flatMap 将这些类型的函数链接在一起

如果您需要转换值的多个方面,请编写一系列小的转换函数,使用 flatMap 将它们链接在一起,然后在最后检查是否有任何更改

/// return a new `User` value with the first name changed to "hamburgers".
/// If the first name is already "hamburgers", do nothing.
func updateFirstName(_ user: User) -> Changeable<User> {
    /* implementation omitted */
}

/// return a new `User` value with the last name changed to "kale".
/// If the last name is already "kale", do nothing.
func updateLastName(_ user: User) -> Changeable<User> {
    /* implementation omitted */
}

let myUser = User(firstName: "hamburgers", lastName: "kale")
Changeable(value: myUser)
    .flatMap(updateFirstName)
    .flatMap(updateLastName) // .hasChanged => false

let someoneElse = User(firstName: "peter", lastName: "kale")
Changeable(value: someoneElse)
    .flatMap(updateFirstName)
    .flatMap(updateLastName) // .hasChanged => true

4. 通过使用 >>- (“bind”) 运算符添加函数式特性

>>- 只是 flatMap 的同义词。它的工作方式相同,但您不必键入那么多括号

let myUser = User(firstName: "hamburgers", lastName: "kale")
Changeable(value: myUser)
    >>- updateFirstName
    >>- updateLastName // .hasChanged => false (same code as above)

5. 使用 Changeable.write 轻松编写可链接的转换函数

updateFirstName 这样手工编写转换函数可能很耗时。对于快速更新,请使用 Changeable.write 来生成转换函数,就像上面那样,从 Swift 键路径构建。

let myUser = User(firstName: "hamburgers", lastName: "kale")
Changeable(value: myUser)
    >>- Changeable.write("hamburgers", at: \.firstName)
    >>- Changeable.write("kale", at: \.lastName) // .hasChanged => false (same overall transformation as above)

6. 通过闭包值将领域逻辑合并到 Changeable.write 函数中

但是,复杂的更改可能需要比上面的简单语法更多。此代码片段使用来自封闭作用域的标志来确定是否写入其更改之一

let makeTheChange = true

let myUser = User(firstName: "hamburgers", lastName: "kale")
Changeable(value: myUser)
    >>- Changeable.write("peter", at: \.firstName, shouldChange: { _, _ in makeTheChange })
    >>- Changeable.write("kale", at: \.lastName)

7. 在方便时使用局部突变

如果您的 Changeable 值是 var,您可以直接对其进行 .write 操作,而无需进行任何 flatMap>>- 操作。此代码片段还使用函数式简写 pure 来快速将普通的 Person 值“提升”为 Changeable

var mutateMe = pure(Person(firstName: "hamburgers", lastName: "kale"))
mutateMe.write("hamburgers", at: \.firstName)
mutateMe.write("kale", at: \.lastName)
mutateMe.hasChanged // => false

Reducers

1. Reducer 为 reducing 函数命名

Sequence.reduce 是一个强大的函数,但传递给它的函数通常只是内联编写的。由于这些函数可能非常有用,请将它们包装在 Reducer 中以命名它们、共享它们和传递它们

let sumOfIntegers: Reducer<Int, Int> = .nextPartialResult { sum, integer in
    return sum + integer
}

[1, 2, 3].reduce(0, sumOfIntegers) // => 6

2. 使用 followed(by:) 将 reducer 链接在一起

如果类型匹配,则可以通过将小的 reducer 链接在一起来构建更大的 reducer。它们按顺序执行

let productOfIntegers: Reducer<Int, Int> = .nextPartialResult { product, integer in
    return product * integer
}

let bigReducer = sumOfIntegers.followed(by: productOfIntegers)

[1, 2, 3].reduce(0, bigReducer) // => 27

3. 使用 <> 运算符将多个 reducer 链接在一起,而无需担心括号

<> 只是 followed(by:) 的同义词,并且可以证明此操作是结合律的。因此,当您想要将许多 reducer 链接在一起时,请使用 <>,并且无需使用括号

let appendIntValue: Reducer<[String], Int> = .nextPartialResult { arr, integer in arr + ["\(integer)"] }
let appendIntValuePlus1: Reducer<[String], Int> = .nextPartialResult { arr, integer in arr + ["\(integer + 1)"] }
let appendIntValuePlus2: Reducer<[String], Int> = .nextPartialResult { arr, integer in arr + ["\(integer + 2)"] }

[1, 10, 100].reduce(
    [],
    appendIntValue <> appendIntValuePlus1 <> appendIntValuePlus2
) // => ["1", "2", "3", "10", "11", "12", "100", "101", "102"]

4. 现有的 reducer 可以使用 pullback 适配到新类型

如果您有一个 reducer 需要 X 类型的值,但您手头只有 Y 类型的值,请编写一个将 Y 转换为 X 的函数,然后使用 pullback 适配您的 reducer。适配后的 reducer 将能够处理新的值

let appendIntValue: Reducer<[String], Int> = .nextPartialResult { arr, integer in arr + ["\(integer)"] }

func getCount(of string: String) -> Int { return string.count }

let appendCountValue = appendIntValue.pullback(getCount) // this reducer has been adapted
["a", "aa", "aaa"].reduce([], appendCountValue) // => ["1", "2", "3"]

5. inout Reducers

inout Reducer(闭包的第一个参数是可变的 reducer)可能非常方便。实际上,我们的 reducer 默认是 inout 的:

let reducer: Reducer<[String], String> = .init { arr, item in arr.append(item) }

(请注意此处使用了 .init 而不是之前示例中使用的 .nextPartialResult

6. 统一表示

在底层,所有 reducer 都是 inout 类型的。这意味着无论 reducer 如何初始化,它都可以与其他所有 reducer 互操作

let reducer: Reducer<[String], String> = .init { arr, item in arr.append(item) }

let reduceCaps: Reducer<[String], String> = .nextPartialResult { arr, item in
    return arr + [item.uppercased()]
}

let bigReducer = reducer <> reduceCaps // this is ok
["foo", "bar", "baz"].reduce([], bigReducer)

函数式粘合剂

Prelude.swift 包含我们对 curryconstflip|> 的实现

运算符