1. Introduction
今天我们来谈谈setters! 因此,问题是,在我们的应用程序中,我们经常遇到复杂的、深度嵌套的数据结构,我们希望能够修改这些结构的一部分,同时保持其他部分不变。此外,我们希望能够以一种简单、干净、可组合的方式来完成它,“composable”的意思是,如果我们有两种修改结构部分的方法,我应该能够将它们组合成一个东西,同时修改这两个部分。
2. Tuples
让我们看看一些足够简单的东西:
let pair = (42, "Swift")
我们转换这个元组的方法是什么? 比如,如果我们想用老朋友incr只增加第一个组件,会怎样?我们可以做:
(incr(pair.0), pair.1) // (43, "Swift")
我们还可以创建一个专门对元组进行操作的函数。我们甚至可以让它更通用一些:
func incrFirst<A>(_ pair: (Int, A)) -> (Int, A) {
return (incr(pair.0), pair.1)
}
incrFirst(pair) // (43, "Swift")
它们都不是非常可重用的,因为它们最终都绑定到incr函数。让我们把它抽象一点。我们要定义一种超一般的方法来转换元组的第一个元素,利用我们对高阶函数的知识,我们知道我们想让变换部分先出现因为它类似于configuration。
func first<A, B, C>(_ f: @escaping (A) -> C) -> ((A, B)) -> (C, B) {
return { pair in
return (f(pair.0), pair.1)
}
}
第一个函数通过将(A) -> C的变换应用到第一个元素上,将其提升到元组上的变换世界。应用这个函数很简单:
first(incr)(pair) // (43, "Swift")
我们可以应用两次
first(incr)(first(incr)(pair)) // (44, "Swift")
看起来不太好,但我们可以使用|>将这些setter连接起来:
pair
|> first(incr)
|> first(incr)
// (44, "Swift")
这里一个很酷的事情是,first是适当的泛型,我们甚至可以改变pair的第一个组件的类型:
pair
|> first(incr)
|> first(String.init)
// ("43", "Swift")
这里我们增加了第一个元素,然后将Int型转换为String型。我们也可以为元组的第二个元素定义这样一个版本:
func second<A, B, C>(_ f: @escaping (B) -> C) -> ((A, B)) -> (A, C) {
return { pair in
return (pair.0, f(pair.1))
}
}
现在我们可以应用一些额外的有趣的转换,比如转换第二个组件!
pair
|> first(incr)
|> first(String.init)
|> second { $0 + "!" }
("43", "Swift!")
我们还可以进入元组的第二部分并将其大写。
pair
|> first(incr)
|> first(String.init)
|> second { $0.uppercased() }
("43", "SWIFT")
我们甚至可以在高阶函数中使用我们的助手。
pair
|> first(incr)
|> first(String.init)
|> second(zurry(flip(String.uppercased)))
// ("43", "SWIFT")
现在我们有了一个完整的数据管道,甚至不需要一对数据就可以存在!
first(incr)
>>> first(String.init)
>>> second(zurry(flip(String.uppercased)))
// (Int, String) -> (String, String)
这是以前很难看到的代码重用。
事实上,如果是普通的ole mutation,它会是什么样子呢? 我们可以做一个深拷贝,并增加第一个值。
var copyPair = pair
copyPair.0 += 1 // 43
我们还可以改变第二个元素,并将其大写。
copyPair.1 = copyPair.1.uppercased()
不过,我们还没有将第一个整数转换为字符串。
copyPair.0 = String(copyPair.0)
// Cannot assign value of type 'String' to type 'Int'
这根本不管用! 我们的可变类型是固定的,我们不能直接将其部分更改为新类型。在这个世界上,要做到这一点,我们必须从头开始创建一个全新的副本。
我们也看到一些代数定律,让人想起过去的一些情节。例如,与第一次应用两次转换不同,我们可以将这些转换组合在一起,然后第一次应用:
pair
|> first(incr >>> String.init)
// ("43", "Swift")
跟我一起说: The composition of the first is the first of the composition! 这是我们第二次看到这种形状。第一个是map,我们说map的合成是合成的map。我们会一遍又一遍地看到这个形式,不久我们就会给它起一个恰当的名字!
3. Nested tuples
现在让我们尝试一些更复杂的东西。假设我们有一个嵌套的元组:
let nested = ((1, true), "Swift")
让我们把里面的true否定为false。我们可以尝试拼凑我们现在拥有的lil函数:
nested
|> first { pair in pair |> second { !$0 } }
// ((1, false), "Swift")
它是有效的,但看起来不是很好。我们可以删除named pair参数并使用$0:
nested
|> first { $0 |> second { !$0 } }
仍然不太棒了!应该有一个更好的方法来组成这些first和second setters。答案是,就像本系列的大多数内容一样,函数组合! 对于函数,您通常只能做两件事:应用于一个值,并组合它们!
我们可能会忍不住这样写:
nested
|> (first >>> second) { !$0 }
它读起来就像我们深入到元组的第一部分,然后深入到嵌套元组的第二部分,但这不能编译。
Generic parameter 'B' could not be inferred
不幸的是,这不能编译,但我们手边有一个快速的组合修复。现在这个组合可能有点像一场心灵之旅,所以请容忍我们。这可能是一次旅行的原因是在我们如何看待这个操作和我们如何执行转换之间有一点脱节。
我们可以把顺序颠倒一下。
nested
|> (second >>> first) { !$0 }
// ((1, false), "Swift")
它编译并生成我们期望的值! 这是怎么回事?
从视觉上看,我们认为这种嵌套转换是通过使用第一个函数深入第一层,然后通过使用第二个函数深入下一层,然后对布尔值求反,但在执行转换时,我们实际上在做相反的事情: 我们首先转换最内层的值,然后通过代入从第一次转换得到的结果来转换最外层的值。从这个意义上说,这个顺序更正确。
组合setter函数对应于如何深入嵌套数据结构。
4. Introducing <<<
好吧,我们没有理由不能定义一种指向另一个方向的构图形式! 实际上,让我们这样做,并称之为反向合成:
precedencegroup BackwardsComposition {
associativity: left
}
infix operator <<<: BackwardsComposition
func <<< <A, B, C>(g: @escaping (B) -> C, f: @escaping (A) -> B) -> (A) -> C {
return { x in
g(f(x))
}
}
现在我们更新之前的setter:
nested
|> (first <<< second) { !$0 }
((1, false), "Swift")
现在,它会编译、执行我们期望的内容,并且它读起来更像我们在视觉上所期望的:进入第一个组件,然后进入第二个组件,然后求反。
尽管反向箭头有点熟悉,但您可以将其看作是通过管道向反方向引导转换函数通过嵌套结构。
现在你知道为什么我们说setter的合成有点像一场思维旅行了:it goes backwards! 乍一看,这似乎很奇怪,但事实是,这段代码经过编译,并且组合中涉及的所有函数都是完全通用的,这基本证明了这是这些setter组合的正确方式。他们不可能以任何其他方式结合在一起。
至少我们对他们为什么要逆向创作有了一些直觉,随着时间的推移,我们会对这个事实感到更舒服。 也许有一天我们会在其他地方看到这些形状我们可以用直觉。 事实上,你和我在建造这个网站的时候遇到过一模一样的东西。 在这个网站的Swift代码库中,我们有一个“中间件”的想法,它就像一个奇特的功能,然后我们有一个“中间件变压器”的想法,它就像中间件之间的功能。经过一段时间的努力,我们最终发现他们是逆向创作的。出于同样的原因,这些都是反向创作的。
在继续之前,让我们确保向自己证明,引入另一个算子是值得的。它满足了>>>的所有原因:
- ✅ 该操作员目前不在Swift中。
- ✅ 这个操作符在Haskell和PureScript中使用,并且有一个很好的形状。
- ✅ 它解决了一个普遍的问题,逆合成函数。
我们想要<<和>>>的原因与我们想要<和>的原因相同。有时使用翻转的版本在语义上更有意义。
好的,现在我们已经建立了setter逆向组合的力量和直觉,让我们稍微弯曲一下。
我们可以深入到第一个元素并递增它,而不是深入到嵌套元组的第二个元素。
nested
|> (first <<< first)(incr)
// ((2, true), "Swift")
我们可以延长链,否定我们的true,并在我们最喜欢的语言后面加一个感叹号。
nested
|> (first <<< first) { $0 + 1 }
|> (first <<< second) { !$0 }
|> second { $0 + "!" }
// ((2, false), "Swift!")
令人惊讶的是,我们可以把整个转换过程存储在一个变量中,而不需要用到任何数据。 当形状没有改变时,可以使用<>来表示处理的是单一类型。
let transformation = (first <<< second) { !$0 }
<> (first <<< first) { $0 + 1 }
<> second { $0 + "!" }
// ((Int, Bool), String) -> ((Int, Bool), String)
当我们想要使用它的时候,我们只要通过管道传递我们的值:
nested |> transformation
// ((2, false), "Swift!")
这展示了如何将setter建模为简单函数,从而生成一些非常可组合的东西,从而使代码易于重用。
5. Arrays
还有另外一种添加嵌套层的方法,函数setter允许我们轻松地遍历这一层。 如果我们想要将数组添加到混合中呢?
(42, ["Swift", "Objective-C"])
我们如何深入元组的第二个组件,然后深入数组的元素,然后在其中执行转换呢?
为了理解我们如何做到这一点,让我们后退一步,看看一些形状。
到目前为止,我们所有的setter函数都有这样的形状:
// ((A) -> B) -> (S) -> T
换句话说,we are lifting a transformation on parts (A) -> B up to a transformation on wholes (S) -> T。
例如,first和second有以下形状:
// ((A) -> B) -> ((A, C)) -> (B, C)
// ((A) -> B) -> ((C, A)) -> (C, B)
这告诉我们,对元组第一个组件的转换可以提升为对整个元组的转换。
这个形状在数组中是什么样子的? 让我们复制粘贴其中一个,并将元组替换为数组:
// ((A) -> B) -> ([A]) -> [B]
这看起来很熟悉!这是我们在上一集定义的自由map函数。它正是一个将函数(A) -> B提升到数组([A]) -> [B]的函数。
func map<A, B>(_ f: @escaping (A) -> B) -> ([A]) -> [B] {
return { xs in xs.map(f) }
}
在某种意义上,map是一个类似setter的函数。 它对数组的各个部分进行转换,以获得对数组本身的转换。
既然我们知道map是一个类似setter的函数,就像我们的first和second函数一样,我们可以以各种方式将它们组合在一起。 可以使用map转换嵌套数组。
(42, ["Swift", "Objective-C"])
|> (second <<< map) { $0 + "!" }
// (42, ["Swift!", "Objective-C!"])
Which is neat! 我们可以在数组中嵌套这种值!
[(42, ["Swift", "Objective-C"]), (1729, ["Haskell", "PureScript"])]
|> (map <<< second <<< map) { $0 + "!" }
// [(42, ["Swift!", "Objective-C!"]), (1729, ["Haskell!", "PureScript!"])]
Jeez! Wild stuff! 令人印象深刻的是,我们能够毫不费力地改造如此复杂的结构。
6. What’s the point?
好了,我们现在已经探索了一种非常复杂的方法来组合函数setter,我们引入了一个新的运算符,我们已经能够用它做一些非常令人印象深刻的事情,但我们不了解你的情况,我们不是一整天都在转换嵌套的元组。为什么这是有用的?
元组是我们可以用来转换的最简单的结构。它只需要很少的设置就能开始运行。 我们还一直在处理嵌套结构,比如具有数组值的结构体,而这些值也可以具有数组、可选或枚举。 我们今天讨论的是一个通用框架,在这个框架中我们可以理解如何遍历嵌套结构并执行转换。
为了看看如果没有这个可组合setter会有多混乱让我们以命令式的方式重新创建最后一个例子。
let data = [
(42, ["Swift", "Objective-C"]),
(1729, ["Haskell", "PureScript"])
]
data.map { ($0.0, $0.1.map { $0 + "!" }) }
它很短,但很乱!
下一步是研究嵌套结构,看看这些思想是如何应用的。 然而,我们希望慢慢来,所以我们将在这里停止,并在下一集介绍结构体。事实证明,有一个美妙的Swift特性可以帮助我们,而且由于这个特性,Swift在编程语言世界中是独一无二的。但是,没有剧透…下次见!