Self T T (initial: T,combine: (T,T
.Generator.Element) -> (accumulator: Int,current: Int) ) -> Int @H_502_144@ 此刻,我们拥有一个初始值(Initial value)以及一个闭包(返回值类型和初始值类型一致)。函数最后的返回值同样和初始值类型一致,为return 。
假设我们现在要实现一个 reduce *** 作 — 对一个整数列表值做累加运算,方案如下:
combinator} -> 1 { 2 accumulator + current reduce [0 ,// 执行步骤如下 ,31);">3 ].1 (return ,combine: combinator)0 combinator(1 ) { 1 2 + 1 } = 2 combinator(3 ) { 3 + 3 } = 3 combinator(6 ,31);">3 ) { 6 + [1,3]
} = 结合(Combinator) = 累加器(Accumulator) @H_502_144@ Int
中的每个元素都将调用一次Optional<Int>
函数进行处理。同时我们使用reduce
变量实时记录递增状态(递增并非是指加法),这里是一个整型值。
接下来,我们重新实现那些函数式编程的「伙伴」(自己来写 map、flatMap 和 filter 函数)。简便起见,所有这些方法都是对// 重新定义一个 map 函数 或rmap 进行 *** 作的;换言之,我们此刻不考虑泛型。另外牢记下面的实现只是为了展示(Int) 的实现过程。原生的 Swift 实现相比较下面 reduce 的版本,速度要快很多1。不过,Reduce 能在不同的问题中表现得很好,之后会进一步地详述。
Map Int Int (elements: [Int],transform: return -> reduce ) -> [Int ] { var elements.Int ([Int ](),combine: { (Int acc: [in ],obj: acc.append(transform(obj)) ) -> [return ] }) } print acc 4 0 2 (rmap([// [2,4,6,8] ],transform: { $reduce
* elements.reduce...
}))[Int]()
@H_502_144@ 这个例子能够很好地帮助你理解combinator
的基础知识。
首先,elements 序列调用 reduce 方法:acc: [Int]
。 然后,我们传入初始值给累加器(Accumulator),即一个 Int 类型空数组(obj: Int
)。 接着,我们传入combinator
闭包,它接收两个参数:第一个参数为 accumulator,即map
;第二个参数为从序列中取得的当前对象reduce
(译者注:对序列进行遍历,每次取到其中的一个对象 obj)。 Int 闭包体中的实现代码非常简单。我们对 obj 做变换处理,然后添加到累加器 accumulator 中。最后返回 accumulator 对象。 相比较调用Int 方法,这种实现代码看起来有点冗余。的确如此!但是,上面这个版本相当详细地解释了// 0 表示第一个传入参数, 表示第二个传入参数,依次类推... 方法是怎么工作的。我们可以对此进行简化。
(Int) -> 1 ) -> [} ] { +
这里有个地方需要引起注意: + [transform($Int )]})
Int 依旧能够正常运行。这个版本都有哪些不同呢?实际上,我们使用了 Swift 中的小技巧,Int 运算符能够对两个序列进行加法 *** 作。因此in 。倘若你正在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增: var ac: [// 作者提倡使用这种,因为执行速度更快 ],b: ac.append(transform(b)) ) -> [return ] }) } reduce
filter
ac (Int) Int @H_502_144@ 为了进一步加深对Int 的理解,我们将继续重新实现guard 方法。
rflatMaplet -> 1 ?) -> [else ] { combine: { return 0 m = transform($0 ) } { print $guard } 0 + [m]}) 3 return (rflatMap([nil $guard
!= filter (Int) Bool }; 这里 rflatMap 和 rmap 主要差异在于,前者增加了一个Int 表达式确保可选类型始终有值(换言之,摒弃那些 nil 的情况)。 Filter rFilter guard : filter -> 0 ) -> [1 ] { } print ($4 + [$filter ]}) 0 2 (rFilter([0 ,31);">6 ],// [4,6] : { $reduce
% map
== filter
}))4 @H_502_144@ 依旧难度不大。我们再次使用 guard 表达式确保满足筛选条件。
到目前为止,// 10 方法看起来更像是+
或combinator
的复杂版本,除此之外然并卵。不过,所结合的内容不需要是一个数组,它可以是其他任何类型。这使得我们依靠一种简单的方式,就可以轻松地实现各种 reduction *** 作。
Reduce 范例 首先介绍我最喜欢的数组元素求和范例:
// 初始值 initial 为 0,每次遍历数组元素,执行 + *** 作[lhs(left-hand sIDe,等式左侧)
].rhs(Right-hand sIDe,等式右侧)
@H_502_144@ 仅传入reduce
作为一个// 24 函数是有效的,它仅仅是对5 和1 做加法处理,最后返回结果值,这完全满足0 函数的要求。
另外一个范例:通过一组数字计算他们的乘积:
// 初始值 initial 为 1,每次遍历数组元素,执行 * *** 作[// 5,3,1 @H_502_144@ 甚至我们可以反转数组:
// $0 指累加器(accumulator),$1 指遍历数组得到的一个元素[typealias ].Acc ] + $Int }) Int @H_502_144@ 最后,来点有难度的任务。我们想要基于某个标准对列表做划分(Partition)处理:
// 为元组定义个别名,此外 Acc 也是闭包传入的 accumulator 的类型partition (Int) = (l: [Bool ],r: [Acc ])return (lst: [Int],criteria: reduce -> Int ) -> Int { Acc lst.Int ((l: [Acc ](),r: [in ]()),combine: { (ac: if ,o: return ) -> else return } criteria(o) { }) (l: ac.l + [o],r: ac.r) } } { partition (r: ac.r + [o],l: ac.l) 5 //: ([2,8],[1,5,7,9]) tuple
reduce
([tuple
,31);">6 ,31);">7,31);">8,31);">9],criteria: { $map
@H_502_144@ 上面实现中最有意思的莫过于我们使用filter
作为 accumulator。你会渐渐发现,一旦你尝试将map 进入到日常工作流中,3 是一个不错的选择,它能够将数据与 reduce *** 作快速挂钩起来。
执行效率对比:Reduce vs. 链式结构 reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,filter 和0 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种 *** 作往往伴随着性能损失,比如以下代码:
[reduce
({ $Int }).Int ({ $Int }). @H_502_144@ 除了毫无意义之外,它还浪费了 cpu 周期。初始序列(即 [0,4])被重复访问了三次之多。首先是 map,接着 filter,最后对数组内容求和。其实,所有这一切 *** 作我们能够使用in 完全替换实现,极大提高执行效率:
// 这里只需要遍历 1 次序列足矣[if ,r: 3 ) -> 0 return 3 (r + else ) % return { } ac + r + }) } var { 0 ac for in @H_502_144@ 这里给出一个快速的基准运行测试,使用以上两个版本以及 for-loop 方式对一个容量为 100000 的列表做处理 *** 作:
// for-loop 版本Array ux = 0 100000 i if 0 (3 ... } ) { } (i + 测试结果 { ux += (i + reduce
) for-loop
reduce
@H_502_144@ Array
正如你所看见的,100000 版本的执行效率和reverse *** 作非常相近,且是链式 *** 作的一半时间。
不过,在某些情况中,链式 *** 作是优于prefix 的。思考如下范例:
3 (// 0.027 Seconds ).Int ().Int (in )0 @H_502_144@ reduce([],r: return ) -> [// 2.927 Seconds ] accumulator ac.insert(r + ac
) reduce
ac }).reduce
@H_502_144@ 这里,注意到使用链式 *** 作花费 0.027s,这与 reduce *** 作的 2.927s 形成了鲜明的反差,这究竟是怎么回事呢?@L_404_3@
Reddit 网站的搜索结果指出,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 参数
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> (count: Int,age: float) { // 在函数内定义别名让函数更加简洁 typealias Acc = (count: Int,age: float) // reduce 结果暂存为临时的变量 let u = persons.reduce((count: 0,age: 0.0)) { (ac: Acc,p) -> Acc in // 获取地区和年龄 guard let personState = p["city"],personAge = p["age"] // 确保选出来的是来自正确的洲 where personState.hasSuffix(state) // 如果缺失年龄或者地区,又或者上者比较结果不等,返回 else { return ac } // 最终累加计算人数和年龄 return (count: ac.count + 1,age: ac.age + float(personAge)) } // 我们的结果就是上面的人数和除以人数后的平均年龄 return (age: u.age / float(u.count),count: u.count)}print(infoFromState(state: "CA",persons: persons))// prints: (count: 3,age: 34.3333) 将为 0…100000 范围内的每个元素都执行一次复制 *** 作。有关对此更好、更详细的解释请看这篇Airspeedvelocity博客文章。 因此,当我们试图使用tuple
来替换掉一组 *** 作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。
现在,可以回到我们的初始问题:计算人口总数和平均年龄。请试着用作为 accumulator 记录状态值。除此之外,代码读起来简明易懂。 来解决吧。
再一次尝试来写 infoFromState 函数 Acc typealias Acc = (count: Int,age: float)
和早前的范例一样,我们再次使用了reduce
亦或 同时,我们在函数体中定义了一个别名reduce
:// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素 ,起到了简化类型注释的作用。
总结 本文是对// 倘若当前元素小于初始值,初始值就会替换成当前元素 方法的一个简短概述。倘若你不想将过多函数式方法通过链式结构串联起来调用,// 示意写法: initial = min(initial,elem) 是数据的输出形式与传入数据的形式不一致时,reduce 就相当有用了。最后,我将向你展示通过使用 reduce 的各种范例来结束本文,希望能为你带来些许灵感。
更多范例 以下范例展示了9 的其他使用案例。请记住例子只作为展示教学使用,即它们更多地强调 reduce 的使用方式,而非为你的代码库提供通用的解决方法。大多数范例都可以通过其他更好、更快的方式来编写(即通过 extension 或 generics)。并且这些实现方式已经在许多 Swift 库中都有实现,诸如SwiftSequence以及Dollar.swift
Minimum 返回列表中的最小项。显然,// 初始值为 Int.max,传入闭包为 min:求两个数的最小值Int max min [集合(Set)
,31);">reduce (Int .if ,combine: contains ) @H_502_144@ Unique 剔除列表中重复的元素。当然,最好的解决方式是使用return 。
7].else ],168);">inreturn a.} (b) { }) a } // prints: 1,7 { Hashable
a + [b] <T,H: Hashable> (T) H @H_502_144@ Group By 遍历整个列表,通过一个鉴别函数对列表中元素进行分组,将分组后的列表作为结果值返回。问题中的鉴别函数返回值类型需要遵循H 协议,这样我们才能拥有不同的键值。此外保留元素的排序,而组内元素排序则不一定被保留下来。
groupbyT (items: [T],f: return -> reduce ) -> [H : [T ]] { T items.H ([:],168);">var ac: [T : [in ]],o: // o 为遍历序列的当前元素 ) -> [let : [// 通过 f 函数得到 o 对应的键值 ]] if var c h = f(o) // 说明 o 对应的键值已经存在,只需要更新键值对应的数组元素即可 c c else = ac[h] { // 说明 o 对应的键值不存在,需要为字典新增一个键值,对应值为 [o] ac.updateValue([o],forKey: h) .append(o) ac.updateValue( } ,forKey: h) } return { }) } print 10 ac 3 // prints: [2: [2,8,11],0: [3,12],1: [1,10]] print (groupby(["Carl" ,31);">11 ,31);">12],f: { $"Cozy" }))// prints: ["C" : ["Carl","Cozy"],"B" : ["Bethlehem","Belem","Brand"],"Z" : ["Zara"]] items
(groupby([count
,element
,0);">"Bethlehem" ,0);">"Belem",0);">"Brand",0);">"Zara"],31);">0.characters.first! }))<T> @H_502_144@ Interpose 函数给定一个count 数组,每隔1 个元素插入T 元素,返回结果值。下面的实现确保了 element 仅在中间插入,而不会添加到数组尾部。
interpose// cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1 typealias : Int = Acc ) -> [T ] { Int Int reduce = (ac: [1 ],cur: Acc ,cnt: T ) Acc ((ac: [],cur: in ),combine: { (a: switch ,o: // 此时遍历的当前元素为序列中的最后一个元素 ) -> case let where a { 1 count return (ac,cur,31);">_ ) // 满足插入条件 (cur+c ) == items.where : c (ac + [o],31);">0) count return ) 1 // 执行下一步 == c : c (ac + [o,element],cur + 1 ) } }).ac ): } + print ) 5 9 // : [1,5] count (interpose([2 ],element: <T> ))(List1: [T],List2: [T]) T : // Zip2Sequence 返回 [(List1,List2)] 是一个数组,类型为元组 )) @H_502_144@ @H_583_1404@Interdig 该函数允许你有选择从两个序列中挑选元素合并成为一个新序列返回。
interdig// 也就解释了为什么 combinator 闭包的类型是 (ac: [T],o: (T,T)) -> [T] return -> [Zip2Sequence ] { T T T T (List1,List2).in ],o: (return ,1 )) -> [ }) ] } print ac + [o.6 ] n
<T> (List: [T],length: Int) (interdig([T ])) @H_502_144@ Chunk 该函数返回原数组分解成长度为typealias 后的多个数组:
chunkAcc T -> [[T ]] { reduce if = (stack: [[return ]],cur: [else ],168);">let l = List.return ((stack: [],cur: [],31);">0),168);">in 1 ac.cnt == length { } (stack: ac.stack + [ac.cur],cur: [o],31);">1 ) } }) { return (stack: ac.stack,cur: ac.cur + [o],cnt: ac.cnt + } ) print 7 // : [[1,2],[3,4],[5,6],[7]] l.stack + [l.cur] accumulator
[+++](chunk([[+++]],length: [+++] @H_502_144@ 函数中使用一个更为复杂的[+++],包含了 stack、current List 以及 count 。
译者注:有关 Reduce 底层实现,请看这篇文章。
原文http://swift.gg/2015/12/10/reduce-all-the-things/
我修改了原文一些由于swift版本变化二导致的一些错误代码,现在能兼容swift2.2
总结 以上是内存溢出为你收集整理的Swift化零为整 :Reduce 详解 全部内容,希望文章能够帮你解决Swift化零为整:Reduce 详解所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 405, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(
概述即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似map、flatMap 或 filter 等函数式编程的构建。而在 Swift 中,这些家伙(map 等几个函数)已经入驻成为「头等公民」了。比起标准的 for 循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的
即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似 map
、 flatMap
或 filter
等函数式编程的构建。而在 Swift 中,这些家伙( map
等几个函数)已经入驻成为「头等公民」了。比起标准的 for
循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的行数,以及使用链式结构构建复杂的逻辑,更显清爽。
本文中,我将介绍附加于 Swift 中的一个非常酷的函数:「Reduce」。相对于map
/filter
函数,reduce
有时不失为一个更好的解决方案。
一个简单的问题
思考这么一个问题:你从 JsON 中获取到一个 persons 列表,意图计算所有来自 California 的居民的平均年龄。需要解析的数据如下所示:
let persons: [[String: String]] = [["name": "Carl Saxon","city": "New York,NY","age": "44"],["name": "Travis Downing","city": "El Segundo,CA","age": "34"],["name": "liz Parker","city": "San Francisco,"age": "32"],["name": "John Newden","city": "New Jersey,"age": "21"],["name": "Hector Simons","city": "San DIEgo,"age": "37"],["name": "Brian Neo","age": "27"]] //注意这家伙没有 city 键值
注意最后一个记录,它遗漏了问题中 person 的居住地 city 。对于这些情况,默默忽略即可…
本例中,我们期望的结果是那三位来自 California 的居民。让我们尝试在 Swift 中使用flatMap
和filter
来实现这个任务。使用flatMap
函数替代map
函数的原因在于前者能够忽略可选值为 nil 的情况。例如flatMap([0,nil,1,2,nil])
的结果是[0,2]
。处理那些没有 city 属性的情况这会非常有用。
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> Int { // 先进行 flatMap 后进行 filter 筛选 // 这非常简单。 ["city"] 是一个可选值,对于那些没有 city 属性的项返回 nil // componentsSeparatedByString 处理键值,例如 "New York,NY" // 最后返回的 ["New York","NY"],last 取到最后的 NY return persons.flatMap( { map
["city"] }) .filter({filter
.hasSuffix(state)}) .count}infoFromState(state: "CA",persons: persons)//#+RESulTS://: 3 一个整型平均值(平均年龄)
不过,现在来思考另外一个难题:你想要获悉居住在 California 的人口数,接着计算他们的平均年龄。如果我们想要在上面函数的基础上尝试做修改,立马会发现难度不小。解决方法倒是有几种,不过大都看起来不适用函数式结构解决方案。倒是通过循环的方式能简单的解决这个问题。
这时候我们要琢磨为啥不适用了,原因很简单:数据的形式(Shape)改变了。而一个整型总和(人口数) 、reduce
函数能够始终保持数据形式的相似性。数组传入,数组返回。当然数组的元素个数和内容可以改变,不过始终是数组形式(Array-shape)。但是,上面所描述的问题要求我们最后转换成的结果是个结构体(Struct),或者说是以元组(Tuple)的形式包含flatMap
和filter
。
对于这种类型的问题,我们可以使用Reduce 来救场。
Reduce
Reduce 是累加器(Accumulator) 或结合(Combinator) 的一种扩展的形式(译者注:后三个函数能干嘛,reduce 就能用另外一种方式实现)。SequenceType
的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个func 来持续记录递增状态。为了实现这个方法,我们会向 reduce 方法中传入一个用于处理序列中每个元素的reduce 闭包 / 函数 / 方法。这听起来有点复杂,不过通过几个例子练手,你就会发现这相当简单。
它是<T> 中的一个方法,看起来是这样的(简化版本):
Self T T (initial: T,combine: (T,T
.Generator.Element) -> (accumulator: Int,current: Int) ) -> Int @H_502_144@ 此刻,我们拥有一个初始值(Initial value)以及一个闭包(返回值类型和初始值类型一致)。函数最后的返回值同样和初始值类型一致,为return 。
假设我们现在要实现一个 reduce *** 作 — 对一个整数列表值做累加运算,方案如下:
combinator} -> 1 { 2 accumulator + current reduce [0 ,// 执行步骤如下 ,31);">3 ].1 (return ,combine: combinator)0 combinator(1 ) { 1 2 + 1 } = 2 combinator(3 ) { 3 + 3 } = 3 combinator(6 ,31);">3 ) { 6 + [1,3]
} = 结合(Combinator) = 累加器(Accumulator) @H_502_144@ Int
中的每个元素都将调用一次Optional<Int>
函数进行处理。同时我们使用reduce
变量实时记录递增状态(递增并非是指加法),这里是一个整型值。
接下来,我们重新实现那些函数式编程的「伙伴」(自己来写 map、flatMap 和 filter 函数)。简便起见,所有这些方法都是对// 重新定义一个 map 函数 或rmap 进行 *** 作的;换言之,我们此刻不考虑泛型。另外牢记下面的实现只是为了展示(Int) 的实现过程。原生的 Swift 实现相比较下面 reduce 的版本,速度要快很多1。不过,Reduce 能在不同的问题中表现得很好,之后会进一步地详述。
Map Int Int (elements: [Int],transform: return -> reduce ) -> [Int ] { var elements.Int ([Int ](),combine: { (Int acc: [in ],obj: acc.append(transform(obj)) ) -> [return ] }) } print acc 4 0 2 (rmap([// [2,4,6,8] ],transform: { $reduce
* elements.reduce...
}))[Int]()
@H_502_144@ 这个例子能够很好地帮助你理解combinator
的基础知识。
首先,elements 序列调用 reduce 方法:acc: [Int]
。 然后,我们传入初始值给累加器(Accumulator),即一个 Int 类型空数组(obj: Int
)。 接着,我们传入combinator
闭包,它接收两个参数:第一个参数为 accumulator,即map
;第二个参数为从序列中取得的当前对象reduce
(译者注:对序列进行遍历,每次取到其中的一个对象 obj)。 Int 闭包体中的实现代码非常简单。我们对 obj 做变换处理,然后添加到累加器 accumulator 中。最后返回 accumulator 对象。 相比较调用Int 方法,这种实现代码看起来有点冗余。的确如此!但是,上面这个版本相当详细地解释了// 0 表示第一个传入参数, 表示第二个传入参数,依次类推... 方法是怎么工作的。我们可以对此进行简化。
(Int) -> 1 ) -> [} ] { +
这里有个地方需要引起注意: + [transform($Int )]})
Int 依旧能够正常运行。这个版本都有哪些不同呢?实际上,我们使用了 Swift 中的小技巧,Int 运算符能够对两个序列进行加法 *** 作。因此in 。倘若你正在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增: var ac: [// 作者提倡使用这种,因为执行速度更快 ],b: ac.append(transform(b)) ) -> [return ] }) } reduce
filter
ac (Int) Int @H_502_144@ 为了进一步加深对Int 的理解,我们将继续重新实现guard 方法。
rflatMaplet -> 1 ?) -> [else ] { combine: { return 0 m = transform($0 ) } { print $guard } 0 + [m]}) 3 return (rflatMap([nil $guard
!= filter (Int) Bool }; 这里 rflatMap 和 rmap 主要差异在于,前者增加了一个Int 表达式确保可选类型始终有值(换言之,摒弃那些 nil 的情况)。 Filter rFilter guard : filter -> 0 ) -> [1 ] { } print ($4 + [$filter ]}) 0 2 (rFilter([0 ,31);">6 ],// [4,6] : { $reduce
% map
== filter
}))4 @H_502_144@ 依旧难度不大。我们再次使用 guard 表达式确保满足筛选条件。
到目前为止,// 10 方法看起来更像是+
或combinator
的复杂版本,除此之外然并卵。不过,所结合的内容不需要是一个数组,它可以是其他任何类型。这使得我们依靠一种简单的方式,就可以轻松地实现各种 reduction *** 作。
Reduce 范例 首先介绍我最喜欢的数组元素求和范例:
// 初始值 initial 为 0,每次遍历数组元素,执行 + *** 作[lhs(left-hand sIDe,等式左侧)
].rhs(Right-hand sIDe,等式右侧)
@H_502_144@ 仅传入reduce
作为一个// 24 函数是有效的,它仅仅是对5 和1 做加法处理,最后返回结果值,这完全满足0 函数的要求。
另外一个范例:通过一组数字计算他们的乘积:
// 初始值 initial 为 1,每次遍历数组元素,执行 * *** 作[// 5,3,1 @H_502_144@ 甚至我们可以反转数组:
// $0 指累加器(accumulator),$1 指遍历数组得到的一个元素[typealias ].Acc ] + $Int }) Int @H_502_144@ 最后,来点有难度的任务。我们想要基于某个标准对列表做划分(Partition)处理:
// 为元组定义个别名,此外 Acc 也是闭包传入的 accumulator 的类型partition (Int) = (l: [Bool ],r: [Acc ])return (lst: [Int],criteria: reduce -> Int ) -> Int { Acc lst.Int ((l: [Acc ](),r: [in ]()),combine: { (ac: if ,o: return ) -> else return } criteria(o) { }) (l: ac.l + [o],r: ac.r) } } { partition (r: ac.r + [o],l: ac.l) 5 //: ([2,8],[1,5,7,9]) tuple
reduce
([tuple
,31);">6 ,31);">7,31);">8,31);">9],criteria: { $map
@H_502_144@ 上面实现中最有意思的莫过于我们使用filter
作为 accumulator。你会渐渐发现,一旦你尝试将map 进入到日常工作流中,3 是一个不错的选择,它能够将数据与 reduce *** 作快速挂钩起来。
执行效率对比:Reduce vs. 链式结构 reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,filter 和0 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种 *** 作往往伴随着性能损失,比如以下代码:
[reduce
({ $Int }).Int ({ $Int }). @H_502_144@ 除了毫无意义之外,它还浪费了 cpu 周期。初始序列(即 [0,4])被重复访问了三次之多。首先是 map,接着 filter,最后对数组内容求和。其实,所有这一切 *** 作我们能够使用in 完全替换实现,极大提高执行效率:
// 这里只需要遍历 1 次序列足矣[if ,r: 3 ) -> 0 return 3 (r + else ) % return { } ac + r + }) } var { 0 ac for in @H_502_144@ 这里给出一个快速的基准运行测试,使用以上两个版本以及 for-loop 方式对一个容量为 100000 的列表做处理 *** 作:
// for-loop 版本Array ux = 0 100000 i if 0 (3 ... } ) { } (i + 测试结果 { ux += (i + reduce
) for-loop
reduce
@H_502_144@ Array
正如你所看见的,100000 版本的执行效率和reverse *** 作非常相近,且是链式 *** 作的一半时间。
不过,在某些情况中,链式 *** 作是优于prefix 的。思考如下范例:
3 (// 0.027 Seconds ).Int ().Int (in )0 @H_502_144@ reduce([],r: return ) -> [// 2.927 Seconds ] accumulator ac.insert(r + ac
) reduce
ac }).reduce
@H_502_144@ 这里,注意到使用链式 *** 作花费 0.027s,这与 reduce *** 作的 2.927s 形成了鲜明的反差,这究竟是怎么回事呢?@L_404_3@
Reddit 网站的搜索结果指出,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 参数
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> (count: Int,age: float) { // 在函数内定义别名让函数更加简洁 typealias Acc = (count: Int,age: float) // reduce 结果暂存为临时的变量 let u = persons.reduce((count: 0,age: 0.0)) { (ac: Acc,p) -> Acc in // 获取地区和年龄 guard let personState = p["city"],personAge = p["age"] // 确保选出来的是来自正确的洲 where personState.hasSuffix(state) // 如果缺失年龄或者地区,又或者上者比较结果不等,返回 else { return ac } // 最终累加计算人数和年龄 return (count: ac.count + 1,age: ac.age + float(personAge)) } // 我们的结果就是上面的人数和除以人数后的平均年龄 return (age: u.age / float(u.count),count: u.count)}print(infoFromState(state: "CA",persons: persons))// prints: (count: 3,age: 34.3333) 将为 0…100000 范围内的每个元素都执行一次复制 *** 作。有关对此更好、更详细的解释请看这篇Airspeedvelocity博客文章。 因此,当我们试图使用tuple
来替换掉一组 *** 作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。
现在,可以回到我们的初始问题:计算人口总数和平均年龄。请试着用作为 accumulator 记录状态值。除此之外,代码读起来简明易懂。 来解决吧。
再一次尝试来写 infoFromState 函数 Acc typealias Acc = (count: Int,age: float)
和早前的范例一样,我们再次使用了reduce
亦或 同时,我们在函数体中定义了一个别名reduce
:// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素 ,起到了简化类型注释的作用。
总结 本文是对// 倘若当前元素小于初始值,初始值就会替换成当前元素 方法的一个简短概述。倘若你不想将过多函数式方法通过链式结构串联起来调用,// 示意写法: initial = min(initial,elem) 是数据的输出形式与传入数据的形式不一致时,reduce 就相当有用了。最后,我将向你展示通过使用 reduce 的各种范例来结束本文,希望能为你带来些许灵感。
更多范例 以下范例展示了9 的其他使用案例。请记住例子只作为展示教学使用,即它们更多地强调 reduce 的使用方式,而非为你的代码库提供通用的解决方法。大多数范例都可以通过其他更好、更快的方式来编写(即通过 extension 或 generics)。并且这些实现方式已经在许多 Swift 库中都有实现,诸如SwiftSequence以及Dollar.swift
Minimum 返回列表中的最小项。显然,// 初始值为 Int.max,传入闭包为 min:求两个数的最小值Int max min [集合(Set)
,31);">reduce (Int .if ,combine: contains ) @H_502_144@ Unique 剔除列表中重复的元素。当然,最好的解决方式是使用return 。
7].else ],168);">inreturn a.} (b) { }) a } // prints: 1,7 { Hashable
a + [b] <T,H: Hashable> (T) H @H_502_144@ Group By 遍历整个列表,通过一个鉴别函数对列表中元素进行分组,将分组后的列表作为结果值返回。问题中的鉴别函数返回值类型需要遵循H 协议,这样我们才能拥有不同的键值。此外保留元素的排序,而组内元素排序则不一定被保留下来。
groupbyT (items: [T],f: return -> reduce ) -> [H : [T ]] { T items.H ([:],168);">var ac: [T : [in ]],o: // o 为遍历序列的当前元素 ) -> [let : [// 通过 f 函数得到 o 对应的键值 ]] if var c h = f(o) // 说明 o 对应的键值已经存在,只需要更新键值对应的数组元素即可 c c else = ac[h] { // 说明 o 对应的键值不存在,需要为字典新增一个键值,对应值为 [o] ac.updateValue([o],forKey: h) .append(o) ac.updateValue( } ,forKey: h) } return { }) } print 10 ac 3 // prints: [2: [2,8,11],0: [3,12],1: [1,10]] print (groupby(["Carl" ,31);">11 ,31);">12],f: { $"Cozy" }))// prints: ["C" : ["Carl","Cozy"],"B" : ["Bethlehem","Belem","Brand"],"Z" : ["Zara"]] items
(groupby([count
,element
,0);">"Bethlehem" ,0);">"Belem",0);">"Brand",0);">"Zara"],31);">0.characters.first! }))<T> @H_502_144@ Interpose 函数给定一个count 数组,每隔1 个元素插入T 元素,返回结果值。下面的实现确保了 element 仅在中间插入,而不会添加到数组尾部。
interpose// cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1 typealias : Int = Acc ) -> [T ] { Int Int reduce = (ac: [1 ],cur: Acc ,cnt: T ) Acc ((ac: [],cur: in ),combine: { (a: switch ,o: // 此时遍历的当前元素为序列中的最后一个元素 ) -> case let where a { 1 count return (ac,cur,31);">_ ) // 满足插入条件 (cur+c ) == items.where : c (ac + [o],31);">0) count return ) 1 // 执行下一步 == c : c (ac + [o,element],cur + 1 ) } }).ac ): } + print ) 5 9 // : [1,5] count (interpose([2 ],element: <T> ))(List1: [T],List2: [T]) T : // Zip2Sequence 返回 [(List1,List2)] 是一个数组,类型为元组 )) @H_502_144@ @H_583_1404@Interdig 该函数允许你有选择从两个序列中挑选元素合并成为一个新序列返回。
interdig// 也就解释了为什么 combinator 闭包的类型是 (ac: [T],o: (T,T)) -> [T] return -> [Zip2Sequence ] { T T T T (List1,List2).in ],o: (return ,1 )) -> [ }) ] } print ac + [o.6 ] n
<T> (List: [T],length: Int) (interdig([T ])) @H_502_144@ Chunk 该函数返回原数组分解成长度为typealias 后的多个数组:
chunkAcc T -> [[T ]] { reduce if = (stack: [[return ]],cur: [else ],168);">let l = List.return ((stack: [],cur: [],31);">0),168);">in 1 ac.cnt == length { } (stack: ac.stack + [ac.cur],cur: [o],31);">1 ) } }) { return (stack: ac.stack,cur: ac.cur + [o],cnt: ac.cnt + } ) print 7 // : [[1,2],[3,4],[5,6],[7]] l.stack + [l.cur] accumulator
(chunk([[+++]],length: [+++] @H_502_144@ 函数中使用一个更为复杂的[+++],包含了 stack、current List 以及 count 。
译者注:有关 Reduce 底层实现,请看这篇文章。
原文http://swift.gg/2015/12/10/reduce-all-the-things/
我修改了原文一些由于swift版本变化二导致的一些错误代码,现在能兼容swift2.2
总结 以上是内存溢出为你收集整理的Swift化零为整 :Reduce 详解 全部内容,希望文章能够帮你解决Swift化零为整:Reduce 详解所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 406, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(
概述即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似map、flatMap 或 filter 等函数式编程的构建。而在 Swift 中,这些家伙(map 等几个函数)已经入驻成为「头等公民」了。比起标准的 for 循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的
即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似 map
、 flatMap
或 filter
等函数式编程的构建。而在 Swift 中,这些家伙( map
等几个函数)已经入驻成为「头等公民」了。比起标准的 for
循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的行数,以及使用链式结构构建复杂的逻辑,更显清爽。
本文中,我将介绍附加于 Swift 中的一个非常酷的函数:「Reduce」。相对于map
/filter
函数,reduce
有时不失为一个更好的解决方案。
一个简单的问题
思考这么一个问题:你从 JsON 中获取到一个 persons 列表,意图计算所有来自 California 的居民的平均年龄。需要解析的数据如下所示:
let persons: [[String: String]] = [["name": "Carl Saxon","city": "New York,NY","age": "44"],["name": "Travis Downing","city": "El Segundo,CA","age": "34"],["name": "liz Parker","city": "San Francisco,"age": "32"],["name": "John Newden","city": "New Jersey,"age": "21"],["name": "Hector Simons","city": "San DIEgo,"age": "37"],["name": "Brian Neo","age": "27"]] //注意这家伙没有 city 键值
注意最后一个记录,它遗漏了问题中 person 的居住地 city 。对于这些情况,默默忽略即可…
本例中,我们期望的结果是那三位来自 California 的居民。让我们尝试在 Swift 中使用flatMap
和filter
来实现这个任务。使用flatMap
函数替代map
函数的原因在于前者能够忽略可选值为 nil 的情况。例如flatMap([0,nil,1,2,nil])
的结果是[0,2]
。处理那些没有 city 属性的情况这会非常有用。
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> Int { // 先进行 flatMap 后进行 filter 筛选 // 这非常简单。 ["city"] 是一个可选值,对于那些没有 city 属性的项返回 nil // componentsSeparatedByString 处理键值,例如 "New York,NY" // 最后返回的 ["New York","NY"],last 取到最后的 NY return persons.flatMap( { map
["city"] }) .filter({filter
.hasSuffix(state)}) .count}infoFromState(state: "CA",persons: persons)//#+RESulTS://: 3 一个整型平均值(平均年龄)
不过,现在来思考另外一个难题:你想要获悉居住在 California 的人口数,接着计算他们的平均年龄。如果我们想要在上面函数的基础上尝试做修改,立马会发现难度不小。解决方法倒是有几种,不过大都看起来不适用函数式结构解决方案。倒是通过循环的方式能简单的解决这个问题。
这时候我们要琢磨为啥不适用了,原因很简单:数据的形式(Shape)改变了。而一个整型总和(人口数) 、reduce
函数能够始终保持数据形式的相似性。数组传入,数组返回。当然数组的元素个数和内容可以改变,不过始终是数组形式(Array-shape)。但是,上面所描述的问题要求我们最后转换成的结果是个结构体(Struct),或者说是以元组(Tuple)的形式包含flatMap
和filter
。
对于这种类型的问题,我们可以使用Reduce 来救场。
Reduce
Reduce 是累加器(Accumulator) 或结合(Combinator) 的一种扩展的形式(译者注:后三个函数能干嘛,reduce 就能用另外一种方式实现)。SequenceType
的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个func 来持续记录递增状态。为了实现这个方法,我们会向 reduce 方法中传入一个用于处理序列中每个元素的reduce 闭包 / 函数 / 方法。这听起来有点复杂,不过通过几个例子练手,你就会发现这相当简单。
它是<T> 中的一个方法,看起来是这样的(简化版本):
Self T T (initial: T,combine: (T,T
.Generator.Element) -> (accumulator: Int,current: Int) ) -> Int @H_502_144@ 此刻,我们拥有一个初始值(Initial value)以及一个闭包(返回值类型和初始值类型一致)。函数最后的返回值同样和初始值类型一致,为return 。
假设我们现在要实现一个 reduce *** 作 — 对一个整数列表值做累加运算,方案如下:
combinator} -> 1 { 2 accumulator + current reduce [0 ,// 执行步骤如下 ,31);">3 ].1 (return ,combine: combinator)0 combinator(1 ) { 1 2 + 1 } = 2 combinator(3 ) { 3 + 3 } = 3 combinator(6 ,31);">3 ) { 6 + [1,3]
} = 结合(Combinator) = 累加器(Accumulator) @H_502_144@ Int
中的每个元素都将调用一次Optional<Int>
函数进行处理。同时我们使用reduce
变量实时记录递增状态(递增并非是指加法),这里是一个整型值。
接下来,我们重新实现那些函数式编程的「伙伴」(自己来写 map、flatMap 和 filter 函数)。简便起见,所有这些方法都是对// 重新定义一个 map 函数 或rmap 进行 *** 作的;换言之,我们此刻不考虑泛型。另外牢记下面的实现只是为了展示(Int) 的实现过程。原生的 Swift 实现相比较下面 reduce 的版本,速度要快很多1。不过,Reduce 能在不同的问题中表现得很好,之后会进一步地详述。
Map Int Int (elements: [Int],transform: return -> reduce ) -> [Int ] { var elements.Int ([Int ](),combine: { (Int acc: [in ],obj: acc.append(transform(obj)) ) -> [return ] }) } print acc 4 0 2 (rmap([// [2,4,6,8] ],transform: { $reduce
* elements.reduce...
}))[Int]()
@H_502_144@ 这个例子能够很好地帮助你理解combinator
的基础知识。
首先,elements 序列调用 reduce 方法:acc: [Int]
。 然后,我们传入初始值给累加器(Accumulator),即一个 Int 类型空数组(obj: Int
)。 接着,我们传入combinator
闭包,它接收两个参数:第一个参数为 accumulator,即map
;第二个参数为从序列中取得的当前对象reduce
(译者注:对序列进行遍历,每次取到其中的一个对象 obj)。 Int 闭包体中的实现代码非常简单。我们对 obj 做变换处理,然后添加到累加器 accumulator 中。最后返回 accumulator 对象。 相比较调用Int 方法,这种实现代码看起来有点冗余。的确如此!但是,上面这个版本相当详细地解释了// 0 表示第一个传入参数, 表示第二个传入参数,依次类推... 方法是怎么工作的。我们可以对此进行简化。
(Int) -> 1 ) -> [} ] { +
这里有个地方需要引起注意: + [transform($Int )]})
Int 依旧能够正常运行。这个版本都有哪些不同呢?实际上,我们使用了 Swift 中的小技巧,Int 运算符能够对两个序列进行加法 *** 作。因此in 。倘若你正在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增: var ac: [// 作者提倡使用这种,因为执行速度更快 ],b: ac.append(transform(b)) ) -> [return ] }) } reduce
filter
ac (Int) Int @H_502_144@ 为了进一步加深对Int 的理解,我们将继续重新实现guard 方法。
rflatMaplet -> 1 ?) -> [else ] { combine: { return 0 m = transform($0 ) } { print $guard } 0 + [m]}) 3 return (rflatMap([nil $guard
!= filter (Int) Bool }; 这里 rflatMap 和 rmap 主要差异在于,前者增加了一个Int 表达式确保可选类型始终有值(换言之,摒弃那些 nil 的情况)。 Filter rFilter guard : filter -> 0 ) -> [1 ] { } print ($4 + [$filter ]}) 0 2 (rFilter([0 ,31);">6 ],// [4,6] : { $reduce
% map
== filter
}))4 @H_502_144@ 依旧难度不大。我们再次使用 guard 表达式确保满足筛选条件。
到目前为止,// 10 方法看起来更像是+
或combinator
的复杂版本,除此之外然并卵。不过,所结合的内容不需要是一个数组,它可以是其他任何类型。这使得我们依靠一种简单的方式,就可以轻松地实现各种 reduction *** 作。
Reduce 范例 首先介绍我最喜欢的数组元素求和范例:
// 初始值 initial 为 0,每次遍历数组元素,执行 + *** 作[lhs(left-hand sIDe,等式左侧)
].rhs(Right-hand sIDe,等式右侧)
@H_502_144@ 仅传入reduce
作为一个// 24 函数是有效的,它仅仅是对5 和1 做加法处理,最后返回结果值,这完全满足0 函数的要求。
另外一个范例:通过一组数字计算他们的乘积:
// 初始值 initial 为 1,每次遍历数组元素,执行 * *** 作[// 5,3,1 @H_502_144@ 甚至我们可以反转数组:
// $0 指累加器(accumulator),$1 指遍历数组得到的一个元素[typealias ].Acc ] + $Int }) Int @H_502_144@ 最后,来点有难度的任务。我们想要基于某个标准对列表做划分(Partition)处理:
// 为元组定义个别名,此外 Acc 也是闭包传入的 accumulator 的类型partition (Int) = (l: [Bool ],r: [Acc ])return (lst: [Int],criteria: reduce -> Int ) -> Int { Acc lst.Int ((l: [Acc ](),r: [in ]()),combine: { (ac: if ,o: return ) -> else return } criteria(o) { }) (l: ac.l + [o],r: ac.r) } } { partition (r: ac.r + [o],l: ac.l) 5 //: ([2,8],[1,5,7,9]) tuple
reduce
([tuple
,31);">6 ,31);">7,31);">8,31);">9],criteria: { $map
@H_502_144@ 上面实现中最有意思的莫过于我们使用filter
作为 accumulator。你会渐渐发现,一旦你尝试将map 进入到日常工作流中,3 是一个不错的选择,它能够将数据与 reduce *** 作快速挂钩起来。
执行效率对比:Reduce vs. 链式结构 reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,filter 和0 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种 *** 作往往伴随着性能损失,比如以下代码:
[reduce
({ $Int }).Int ({ $Int }). @H_502_144@ 除了毫无意义之外,它还浪费了 cpu 周期。初始序列(即 [0,4])被重复访问了三次之多。首先是 map,接着 filter,最后对数组内容求和。其实,所有这一切 *** 作我们能够使用in 完全替换实现,极大提高执行效率:
// 这里只需要遍历 1 次序列足矣[if ,r: 3 ) -> 0 return 3 (r + else ) % return { } ac + r + }) } var { 0 ac for in @H_502_144@ 这里给出一个快速的基准运行测试,使用以上两个版本以及 for-loop 方式对一个容量为 100000 的列表做处理 *** 作:
// for-loop 版本Array ux = 0 100000 i if 0 (3 ... } ) { } (i + 测试结果 { ux += (i + reduce
) for-loop
reduce
@H_502_144@ Array
正如你所看见的,100000 版本的执行效率和reverse *** 作非常相近,且是链式 *** 作的一半时间。
不过,在某些情况中,链式 *** 作是优于prefix 的。思考如下范例:
3 (// 0.027 Seconds ).Int ().Int (in )0 @H_502_144@ reduce([],r: return ) -> [// 2.927 Seconds ] accumulator ac.insert(r + ac
) reduce
ac }).reduce
@H_502_144@ 这里,注意到使用链式 *** 作花费 0.027s,这与 reduce *** 作的 2.927s 形成了鲜明的反差,这究竟是怎么回事呢?@L_404_3@
Reddit 网站的搜索结果指出,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 参数
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> (count: Int,age: float) { // 在函数内定义别名让函数更加简洁 typealias Acc = (count: Int,age: float) // reduce 结果暂存为临时的变量 let u = persons.reduce((count: 0,age: 0.0)) { (ac: Acc,p) -> Acc in // 获取地区和年龄 guard let personState = p["city"],personAge = p["age"] // 确保选出来的是来自正确的洲 where personState.hasSuffix(state) // 如果缺失年龄或者地区,又或者上者比较结果不等,返回 else { return ac } // 最终累加计算人数和年龄 return (count: ac.count + 1,age: ac.age + float(personAge)) } // 我们的结果就是上面的人数和除以人数后的平均年龄 return (age: u.age / float(u.count),count: u.count)}print(infoFromState(state: "CA",persons: persons))// prints: (count: 3,age: 34.3333) 将为 0…100000 范围内的每个元素都执行一次复制 *** 作。有关对此更好、更详细的解释请看这篇Airspeedvelocity博客文章。 因此,当我们试图使用tuple
来替换掉一组 *** 作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。
现在,可以回到我们的初始问题:计算人口总数和平均年龄。请试着用作为 accumulator 记录状态值。除此之外,代码读起来简明易懂。 来解决吧。
再一次尝试来写 infoFromState 函数 Acc typealias Acc = (count: Int,age: float)
和早前的范例一样,我们再次使用了reduce
亦或 同时,我们在函数体中定义了一个别名reduce
:// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素 ,起到了简化类型注释的作用。
总结 本文是对// 倘若当前元素小于初始值,初始值就会替换成当前元素 方法的一个简短概述。倘若你不想将过多函数式方法通过链式结构串联起来调用,// 示意写法: initial = min(initial,elem) 是数据的输出形式与传入数据的形式不一致时,reduce 就相当有用了。最后,我将向你展示通过使用 reduce 的各种范例来结束本文,希望能为你带来些许灵感。
更多范例 以下范例展示了9 的其他使用案例。请记住例子只作为展示教学使用,即它们更多地强调 reduce 的使用方式,而非为你的代码库提供通用的解决方法。大多数范例都可以通过其他更好、更快的方式来编写(即通过 extension 或 generics)。并且这些实现方式已经在许多 Swift 库中都有实现,诸如SwiftSequence以及Dollar.swift
Minimum 返回列表中的最小项。显然,// 初始值为 Int.max,传入闭包为 min:求两个数的最小值Int max min [集合(Set)
,31);">reduce (Int .if ,combine: contains ) @H_502_144@ Unique 剔除列表中重复的元素。当然,最好的解决方式是使用return 。
7].else ],168);">inreturn a.} (b) { }) a } // prints: 1,7 { Hashable
a + [b] <T,H: Hashable> (T) H @H_502_144@ Group By 遍历整个列表,通过一个鉴别函数对列表中元素进行分组,将分组后的列表作为结果值返回。问题中的鉴别函数返回值类型需要遵循H 协议,这样我们才能拥有不同的键值。此外保留元素的排序,而组内元素排序则不一定被保留下来。
groupbyT (items: [T],f: return -> reduce ) -> [H : [T ]] { T items.H ([:],168);">var ac: [T : [in ]],o: // o 为遍历序列的当前元素 ) -> [let : [// 通过 f 函数得到 o 对应的键值 ]] if var c h = f(o) // 说明 o 对应的键值已经存在,只需要更新键值对应的数组元素即可 c c else = ac[h] { // 说明 o 对应的键值不存在,需要为字典新增一个键值,对应值为 [o] ac.updateValue([o],forKey: h) .append(o) ac.updateValue( } ,forKey: h) } return { }) } print 10 ac 3 // prints: [2: [2,8,11],0: [3,12],1: [1,10]] print (groupby(["Carl" ,31);">11 ,31);">12],f: { $"Cozy" }))// prints: ["C" : ["Carl","Cozy"],"B" : ["Bethlehem","Belem","Brand"],"Z" : ["Zara"]] items
(groupby([count
,element
,0);">"Bethlehem" ,0);">"Belem",0);">"Brand",0);">"Zara"],31);">0.characters.first! }))<T> @H_502_144@ Interpose 函数给定一个count 数组,每隔1 个元素插入T 元素,返回结果值。下面的实现确保了 element 仅在中间插入,而不会添加到数组尾部。
interpose// cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1 typealias : Int = Acc ) -> [T ] { Int Int reduce = (ac: [1 ],cur: Acc ,cnt: T ) Acc ((ac: [],cur: in ),combine: { (a: switch ,o: // 此时遍历的当前元素为序列中的最后一个元素 ) -> case let where a { 1 count return (ac,cur,31);">_ ) // 满足插入条件 (cur+c ) == items.where : c (ac + [o],31);">0) count return ) 1 // 执行下一步 == c : c (ac + [o,element],cur + 1 ) } }).ac ): } + print ) 5 9 // : [1,5] count (interpose([2 ],element: <T> ))(List1: [T],List2: [T]) T : // Zip2Sequence 返回 [(List1,List2)] 是一个数组,类型为元组 )) @H_502_144@ @H_583_1404@Interdig 该函数允许你有选择从两个序列中挑选元素合并成为一个新序列返回。
interdig// 也就解释了为什么 combinator 闭包的类型是 (ac: [T],o: (T,T)) -> [T] return -> [Zip2Sequence ] { T T T T (List1,List2).in ],o: (return ,1 )) -> [ }) ] } print ac + [o.6 ] n
<T> (List: [T],length: Int) (interdig([T ])) @H_502_144@ Chunk 该函数返回原数组分解成长度为typealias 后的多个数组:
chunkAcc T -> [[T ]] { reduce if = (stack: [[return ]],cur: [else ],168);">let l = List.return ((stack: [],cur: [],31);">0),168);">in 1 ac.cnt == length { } (stack: ac.stack + [ac.cur],cur: [o],31);">1 ) } }) { return (stack: ac.stack,cur: ac.cur + [o],cnt: ac.cnt + } ) print 7 // : [[1,2],[3,4],[5,6],[7]] l.stack + [l.cur] accumulator
(chunk([],length: [+++] @H_502_144@ 函数中使用一个更为复杂的[+++],包含了 stack、current List 以及 count 。
译者注:有关 Reduce 底层实现,请看这篇文章。
原文http://swift.gg/2015/12/10/reduce-all-the-things/
我修改了原文一些由于swift版本变化二导致的一些错误代码,现在能兼容swift2.2
总结 以上是内存溢出为你收集整理的Swift化零为整 :Reduce 详解 全部内容,希望文章能够帮你解决Swift化零为整:Reduce 详解所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 407, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(
概述即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似map、flatMap 或 filter 等函数式编程的构建。而在 Swift 中,这些家伙(map 等几个函数)已经入驻成为「头等公民」了。比起标准的 for 循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的
即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似 map
、 flatMap
或 filter
等函数式编程的构建。而在 Swift 中,这些家伙( map
等几个函数)已经入驻成为「头等公民」了。比起标准的 for
循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的行数,以及使用链式结构构建复杂的逻辑,更显清爽。
本文中,我将介绍附加于 Swift 中的一个非常酷的函数:「Reduce」。相对于map
/filter
函数,reduce
有时不失为一个更好的解决方案。
一个简单的问题
思考这么一个问题:你从 JsON 中获取到一个 persons 列表,意图计算所有来自 California 的居民的平均年龄。需要解析的数据如下所示:
let persons: [[String: String]] = [["name": "Carl Saxon","city": "New York,NY","age": "44"],["name": "Travis Downing","city": "El Segundo,CA","age": "34"],["name": "liz Parker","city": "San Francisco,"age": "32"],["name": "John Newden","city": "New Jersey,"age": "21"],["name": "Hector Simons","city": "San DIEgo,"age": "37"],["name": "Brian Neo","age": "27"]] //注意这家伙没有 city 键值
注意最后一个记录,它遗漏了问题中 person 的居住地 city 。对于这些情况,默默忽略即可…
本例中,我们期望的结果是那三位来自 California 的居民。让我们尝试在 Swift 中使用flatMap
和filter
来实现这个任务。使用flatMap
函数替代map
函数的原因在于前者能够忽略可选值为 nil 的情况。例如flatMap([0,nil,1,2,nil])
的结果是[0,2]
。处理那些没有 city 属性的情况这会非常有用。
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> Int { // 先进行 flatMap 后进行 filter 筛选 // 这非常简单。 ["city"] 是一个可选值,对于那些没有 city 属性的项返回 nil // componentsSeparatedByString 处理键值,例如 "New York,NY" // 最后返回的 ["New York","NY"],last 取到最后的 NY return persons.flatMap( { map
["city"] }) .filter({filter
.hasSuffix(state)}) .count}infoFromState(state: "CA",persons: persons)//#+RESulTS://: 3 一个整型平均值(平均年龄)
不过,现在来思考另外一个难题:你想要获悉居住在 California 的人口数,接着计算他们的平均年龄。如果我们想要在上面函数的基础上尝试做修改,立马会发现难度不小。解决方法倒是有几种,不过大都看起来不适用函数式结构解决方案。倒是通过循环的方式能简单的解决这个问题。
这时候我们要琢磨为啥不适用了,原因很简单:数据的形式(Shape)改变了。而一个整型总和(人口数) 、reduce
函数能够始终保持数据形式的相似性。数组传入,数组返回。当然数组的元素个数和内容可以改变,不过始终是数组形式(Array-shape)。但是,上面所描述的问题要求我们最后转换成的结果是个结构体(Struct),或者说是以元组(Tuple)的形式包含flatMap
和filter
。
对于这种类型的问题,我们可以使用Reduce 来救场。
Reduce
Reduce 是累加器(Accumulator) 或结合(Combinator) 的一种扩展的形式(译者注:后三个函数能干嘛,reduce 就能用另外一种方式实现)。SequenceType
的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个func 来持续记录递增状态。为了实现这个方法,我们会向 reduce 方法中传入一个用于处理序列中每个元素的reduce 闭包 / 函数 / 方法。这听起来有点复杂,不过通过几个例子练手,你就会发现这相当简单。
它是<T> 中的一个方法,看起来是这样的(简化版本):
Self T T (initial: T,combine: (T,T
.Generator.Element) -> (accumulator: Int,current: Int) ) -> Int @H_502_144@ 此刻,我们拥有一个初始值(Initial value)以及一个闭包(返回值类型和初始值类型一致)。函数最后的返回值同样和初始值类型一致,为return 。
假设我们现在要实现一个 reduce *** 作 — 对一个整数列表值做累加运算,方案如下:
combinator} -> 1 { 2 accumulator + current reduce [0 ,// 执行步骤如下 ,31);">3 ].1 (return ,combine: combinator)0 combinator(1 ) { 1 2 + 1 } = 2 combinator(3 ) { 3 + 3 } = 3 combinator(6 ,31);">3 ) { 6 + [1,3]
} = 结合(Combinator) = 累加器(Accumulator) @H_502_144@ Int
中的每个元素都将调用一次Optional<Int>
函数进行处理。同时我们使用reduce
变量实时记录递增状态(递增并非是指加法),这里是一个整型值。
接下来,我们重新实现那些函数式编程的「伙伴」(自己来写 map、flatMap 和 filter 函数)。简便起见,所有这些方法都是对// 重新定义一个 map 函数 或rmap 进行 *** 作的;换言之,我们此刻不考虑泛型。另外牢记下面的实现只是为了展示(Int) 的实现过程。原生的 Swift 实现相比较下面 reduce 的版本,速度要快很多1。不过,Reduce 能在不同的问题中表现得很好,之后会进一步地详述。
Map Int Int (elements: [Int],transform: return -> reduce ) -> [Int ] { var elements.Int ([Int ](),combine: { (Int acc: [in ],obj: acc.append(transform(obj)) ) -> [return ] }) } print acc 4 0 2 (rmap([// [2,4,6,8] ],transform: { $reduce
* elements.reduce...
}))[Int]()
@H_502_144@ 这个例子能够很好地帮助你理解combinator
的基础知识。
首先,elements 序列调用 reduce 方法:acc: [Int]
。 然后,我们传入初始值给累加器(Accumulator),即一个 Int 类型空数组(obj: Int
)。 接着,我们传入combinator
闭包,它接收两个参数:第一个参数为 accumulator,即map
;第二个参数为从序列中取得的当前对象reduce
(译者注:对序列进行遍历,每次取到其中的一个对象 obj)。 Int 闭包体中的实现代码非常简单。我们对 obj 做变换处理,然后添加到累加器 accumulator 中。最后返回 accumulator 对象。 相比较调用Int 方法,这种实现代码看起来有点冗余。的确如此!但是,上面这个版本相当详细地解释了// 0 表示第一个传入参数, 表示第二个传入参数,依次类推... 方法是怎么工作的。我们可以对此进行简化。
(Int) -> 1 ) -> [} ] { +
这里有个地方需要引起注意: + [transform($Int )]})
Int 依旧能够正常运行。这个版本都有哪些不同呢?实际上,我们使用了 Swift 中的小技巧,Int 运算符能够对两个序列进行加法 *** 作。因此in 。倘若你正在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增: var ac: [// 作者提倡使用这种,因为执行速度更快 ],b: ac.append(transform(b)) ) -> [return ] }) } reduce
filter
ac (Int) Int @H_502_144@ 为了进一步加深对Int 的理解,我们将继续重新实现guard 方法。
rflatMaplet -> 1 ?) -> [else ] { combine: { return 0 m = transform($0 ) } { print $guard } 0 + [m]}) 3 return (rflatMap([nil $guard
!= filter (Int) Bool }; 这里 rflatMap 和 rmap 主要差异在于,前者增加了一个Int 表达式确保可选类型始终有值(换言之,摒弃那些 nil 的情况)。 Filter rFilter guard : filter -> 0 ) -> [1 ] { } print ($4 + [$filter ]}) 0 2 (rFilter([0 ,31);">6 ],// [4,6] : { $reduce
% map
== filter
}))4 @H_502_144@ 依旧难度不大。我们再次使用 guard 表达式确保满足筛选条件。
到目前为止,// 10 方法看起来更像是+
或combinator
的复杂版本,除此之外然并卵。不过,所结合的内容不需要是一个数组,它可以是其他任何类型。这使得我们依靠一种简单的方式,就可以轻松地实现各种 reduction *** 作。
Reduce 范例 首先介绍我最喜欢的数组元素求和范例:
// 初始值 initial 为 0,每次遍历数组元素,执行 + *** 作[lhs(left-hand sIDe,等式左侧)
].rhs(Right-hand sIDe,等式右侧)
@H_502_144@ 仅传入reduce
作为一个// 24 函数是有效的,它仅仅是对5 和1 做加法处理,最后返回结果值,这完全满足0 函数的要求。
另外一个范例:通过一组数字计算他们的乘积:
// 初始值 initial 为 1,每次遍历数组元素,执行 * *** 作[// 5,3,1 @H_502_144@ 甚至我们可以反转数组:
// $0 指累加器(accumulator),$1 指遍历数组得到的一个元素[typealias ].Acc ] + $Int }) Int @H_502_144@ 最后,来点有难度的任务。我们想要基于某个标准对列表做划分(Partition)处理:
// 为元组定义个别名,此外 Acc 也是闭包传入的 accumulator 的类型partition (Int) = (l: [Bool ],r: [Acc ])return (lst: [Int],criteria: reduce -> Int ) -> Int { Acc lst.Int ((l: [Acc ](),r: [in ]()),combine: { (ac: if ,o: return ) -> else return } criteria(o) { }) (l: ac.l + [o],r: ac.r) } } { partition (r: ac.r + [o],l: ac.l) 5 //: ([2,8],[1,5,7,9]) tuple
reduce
([tuple
,31);">6 ,31);">7,31);">8,31);">9],criteria: { $map
@H_502_144@ 上面实现中最有意思的莫过于我们使用filter
作为 accumulator。你会渐渐发现,一旦你尝试将map 进入到日常工作流中,3 是一个不错的选择,它能够将数据与 reduce *** 作快速挂钩起来。
执行效率对比:Reduce vs. 链式结构 reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,filter 和0 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种 *** 作往往伴随着性能损失,比如以下代码:
[reduce
({ $Int }).Int ({ $Int }). @H_502_144@ 除了毫无意义之外,它还浪费了 cpu 周期。初始序列(即 [0,4])被重复访问了三次之多。首先是 map,接着 filter,最后对数组内容求和。其实,所有这一切 *** 作我们能够使用in 完全替换实现,极大提高执行效率:
// 这里只需要遍历 1 次序列足矣[if ,r: 3 ) -> 0 return 3 (r + else ) % return { } ac + r + }) } var { 0 ac for in @H_502_144@ 这里给出一个快速的基准运行测试,使用以上两个版本以及 for-loop 方式对一个容量为 100000 的列表做处理 *** 作:
// for-loop 版本Array ux = 0 100000 i if 0 (3 ... } ) { } (i + 测试结果 { ux += (i + reduce
) for-loop
reduce
@H_502_144@ Array
正如你所看见的,100000 版本的执行效率和reverse *** 作非常相近,且是链式 *** 作的一半时间。
不过,在某些情况中,链式 *** 作是优于prefix 的。思考如下范例:
3 (// 0.027 Seconds ).Int ().Int (in )0 @H_502_144@ reduce([],r: return ) -> [// 2.927 Seconds ] accumulator ac.insert(r + ac
) reduce
ac }).reduce
@H_502_144@ 这里,注意到使用链式 *** 作花费 0.027s,这与 reduce *** 作的 2.927s 形成了鲜明的反差,这究竟是怎么回事呢?@L_404_3@
Reddit 网站的搜索结果指出,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 参数
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> (count: Int,age: float) { // 在函数内定义别名让函数更加简洁 typealias Acc = (count: Int,age: float) // reduce 结果暂存为临时的变量 let u = persons.reduce((count: 0,age: 0.0)) { (ac: Acc,p) -> Acc in // 获取地区和年龄 guard let personState = p["city"],personAge = p["age"] // 确保选出来的是来自正确的洲 where personState.hasSuffix(state) // 如果缺失年龄或者地区,又或者上者比较结果不等,返回 else { return ac } // 最终累加计算人数和年龄 return (count: ac.count + 1,age: ac.age + float(personAge)) } // 我们的结果就是上面的人数和除以人数后的平均年龄 return (age: u.age / float(u.count),count: u.count)}print(infoFromState(state: "CA",persons: persons))// prints: (count: 3,age: 34.3333) 将为 0…100000 范围内的每个元素都执行一次复制 *** 作。有关对此更好、更详细的解释请看这篇Airspeedvelocity博客文章。 因此,当我们试图使用tuple
来替换掉一组 *** 作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。
现在,可以回到我们的初始问题:计算人口总数和平均年龄。请试着用作为 accumulator 记录状态值。除此之外,代码读起来简明易懂。 来解决吧。
再一次尝试来写 infoFromState 函数 Acc typealias Acc = (count: Int,age: float)
和早前的范例一样,我们再次使用了reduce
亦或 同时,我们在函数体中定义了一个别名reduce
:// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素 ,起到了简化类型注释的作用。
总结 本文是对// 倘若当前元素小于初始值,初始值就会替换成当前元素 方法的一个简短概述。倘若你不想将过多函数式方法通过链式结构串联起来调用,// 示意写法: initial = min(initial,elem) 是数据的输出形式与传入数据的形式不一致时,reduce 就相当有用了。最后,我将向你展示通过使用 reduce 的各种范例来结束本文,希望能为你带来些许灵感。
更多范例 以下范例展示了9 的其他使用案例。请记住例子只作为展示教学使用,即它们更多地强调 reduce 的使用方式,而非为你的代码库提供通用的解决方法。大多数范例都可以通过其他更好、更快的方式来编写(即通过 extension 或 generics)。并且这些实现方式已经在许多 Swift 库中都有实现,诸如SwiftSequence以及Dollar.swift
Minimum 返回列表中的最小项。显然,// 初始值为 Int.max,传入闭包为 min:求两个数的最小值Int max min [集合(Set)
,31);">reduce (Int .if ,combine: contains ) @H_502_144@ Unique 剔除列表中重复的元素。当然,最好的解决方式是使用return 。
7].else ],168);">inreturn a.} (b) { }) a } // prints: 1,7 { Hashable
a + [b] <T,H: Hashable> (T) H @H_502_144@ Group By 遍历整个列表,通过一个鉴别函数对列表中元素进行分组,将分组后的列表作为结果值返回。问题中的鉴别函数返回值类型需要遵循H 协议,这样我们才能拥有不同的键值。此外保留元素的排序,而组内元素排序则不一定被保留下来。
groupbyT (items: [T],f: return -> reduce ) -> [H : [T ]] { T items.H ([:],168);">var ac: [T : [in ]],o: // o 为遍历序列的当前元素 ) -> [let : [// 通过 f 函数得到 o 对应的键值 ]] if var c h = f(o) // 说明 o 对应的键值已经存在,只需要更新键值对应的数组元素即可 c c else = ac[h] { // 说明 o 对应的键值不存在,需要为字典新增一个键值,对应值为 [o] ac.updateValue([o],forKey: h) .append(o) ac.updateValue( } ,forKey: h) } return { }) } print 10 ac 3 // prints: [2: [2,8,11],0: [3,12],1: [1,10]] print (groupby(["Carl" ,31);">11 ,31);">12],f: { $"Cozy" }))// prints: ["C" : ["Carl","Cozy"],"B" : ["Bethlehem","Belem","Brand"],"Z" : ["Zara"]] items
(groupby([count
,element
,0);">"Bethlehem" ,0);">"Belem",0);">"Brand",0);">"Zara"],31);">0.characters.first! }))<T> @H_502_144@ Interpose 函数给定一个count 数组,每隔1 个元素插入T 元素,返回结果值。下面的实现确保了 element 仅在中间插入,而不会添加到数组尾部。
interpose// cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1 typealias : Int = Acc ) -> [T ] { Int Int reduce = (ac: [1 ],cur: Acc ,cnt: T ) Acc ((ac: [],cur: in ),combine: { (a: switch ,o: // 此时遍历的当前元素为序列中的最后一个元素 ) -> case let where a { 1 count return (ac,cur,31);">_ ) // 满足插入条件 (cur+c ) == items.where : c (ac + [o],31);">0) count return ) 1 // 执行下一步 == c : c (ac + [o,element],cur + 1 ) } }).ac ): } + print ) 5 9 // : [1,5] count (interpose([2 ],element: <T> ))(List1: [T],List2: [T]) T : // Zip2Sequence 返回 [(List1,List2)] 是一个数组,类型为元组 )) @H_502_144@ @H_583_1404@Interdig 该函数允许你有选择从两个序列中挑选元素合并成为一个新序列返回。
interdig// 也就解释了为什么 combinator 闭包的类型是 (ac: [T],o: (T,T)) -> [T] return -> [Zip2Sequence ] { T T T T (List1,List2).in ],o: (return ,1 )) -> [ }) ] } print ac + [o.6 ] n
<T> (List: [T],length: Int) (interdig([T ])) @H_502_144@ Chunk 该函数返回原数组分解成长度为typealias 后的多个数组:
chunkAcc T -> [[T ]] { reduce if = (stack: [[return ]],cur: [else ],168);">let l = List.return ((stack: [],cur: [],31);">0),168);">in 1 ac.cnt == length { } (stack: ac.stack + [ac.cur],cur: [o],31);">1 ) } }) { return (stack: ac.stack,cur: ac.cur + [o],cnt: ac.cnt + } ) print 7 // : [[1,2],[3,4],[5,6],[7]] l.stack + [l.cur] accumulator
(chunk([],length: @H_502_144@ 函数中使用一个更为复杂的[+++],包含了 stack、current List 以及 count 。
译者注:有关 Reduce 底层实现,请看这篇文章。
原文http://swift.gg/2015/12/10/reduce-all-the-things/
我修改了原文一些由于swift版本变化二导致的一些错误代码,现在能兼容swift2.2
总结 以上是内存溢出为你收集整理的Swift化零为整 :Reduce 详解 全部内容,希望文章能够帮你解决Swift化零为整:Reduce 详解所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Swift化零为整:Reduce 详解_app_内存溢出
首页
app
Swift化零为整:Reduce 详解
水射流切割
•
2022-5-27
•
app
•
阅读 28
概述即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似map、flatMap 或 filter 等函数式编程的构建。而在 Swift 中,这些家伙(map 等几个函数)已经入驻成为「头等公民」了。比起标准的 for 循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的
即使早在 Swift 正式发布之前,iOS / Cocoa 开发者都可以使用诸如 ObjectiveSugar 或者 ReactiveCocoa 第三方库,实现类似 map
、 flatMap
或 filter
等函数式编程的构建。而在 Swift 中,这些家伙( map
等几个函数)已经入驻成为「头等公民」了。比起标准的 for
循环,使用函数式编程有很多优势。它们通常能够更好地表达你的意图,减少代码的行数,以及使用链式结构构建复杂的逻辑,更显清爽。
本文中,我将介绍附加于 Swift 中的一个非常酷的函数:「Reduce」。相对于map
/filter
函数,reduce
有时不失为一个更好的解决方案。
一个简单的问题
思考这么一个问题:你从 JsON 中获取到一个 persons 列表,意图计算所有来自 California 的居民的平均年龄。需要解析的数据如下所示:
let persons: [[String: String]] = [["name": "Carl Saxon","city": "New York,NY","age": "44"],["name": "Travis Downing","city": "El Segundo,CA","age": "34"],["name": "liz Parker","city": "San Francisco,"age": "32"],["name": "John Newden","city": "New Jersey,"age": "21"],["name": "Hector Simons","city": "San DIEgo,"age": "37"],["name": "Brian Neo","age": "27"]] //注意这家伙没有 city 键值
注意最后一个记录,它遗漏了问题中 person 的居住地 city 。对于这些情况,默默忽略即可…
本例中,我们期望的结果是那三位来自 California 的居民。让我们尝试在 Swift 中使用flatMap
和filter
来实现这个任务。使用flatMap
函数替代map
函数的原因在于前者能够忽略可选值为 nil 的情况。例如flatMap([0,nil,1,2,nil])
的结果是[0,2]
。处理那些没有 city 属性的情况这会非常有用。
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> Int { // 先进行 flatMap 后进行 filter 筛选 // 这非常简单。 ["city"] 是一个可选值,对于那些没有 city 属性的项返回 nil // componentsSeparatedByString 处理键值,例如 "New York,NY" // 最后返回的 ["New York","NY"],last 取到最后的 NY return persons.flatMap( { map
["city"] }) .filter({filter
.hasSuffix(state)}) .count}infoFromState(state: "CA",persons: persons)//#+RESulTS://: 3 一个整型平均值(平均年龄)
不过,现在来思考另外一个难题:你想要获悉居住在 California 的人口数,接着计算他们的平均年龄。如果我们想要在上面函数的基础上尝试做修改,立马会发现难度不小。解决方法倒是有几种,不过大都看起来不适用函数式结构解决方案。倒是通过循环的方式能简单的解决这个问题。
这时候我们要琢磨为啥不适用了,原因很简单:数据的形式(Shape)改变了。而一个整型总和(人口数) 、reduce
函数能够始终保持数据形式的相似性。数组传入,数组返回。当然数组的元素个数和内容可以改变,不过始终是数组形式(Array-shape)。但是,上面所描述的问题要求我们最后转换成的结果是个结构体(Struct),或者说是以元组(Tuple)的形式包含flatMap
和filter
。
对于这种类型的问题,我们可以使用Reduce 来救场。
Reduce
Reduce 是累加器(Accumulator) 或结合(Combinator) 的一种扩展的形式(译者注:后三个函数能干嘛,reduce 就能用另外一种方式实现)。SequenceType
的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个func 来持续记录递增状态。为了实现这个方法,我们会向 reduce 方法中传入一个用于处理序列中每个元素的reduce 闭包 / 函数 / 方法。这听起来有点复杂,不过通过几个例子练手,你就会发现这相当简单。
它是<T> 中的一个方法,看起来是这样的(简化版本):
Self T T (initial: T,combine: (T,T
.Generator.Element) -> (accumulator: Int,current: Int) ) -> Int @H_502_144@ 此刻,我们拥有一个初始值(Initial value)以及一个闭包(返回值类型和初始值类型一致)。函数最后的返回值同样和初始值类型一致,为return 。
假设我们现在要实现一个 reduce *** 作 — 对一个整数列表值做累加运算,方案如下:
combinator} -> 1 { 2 accumulator + current reduce [0 ,// 执行步骤如下 ,31);">3 ].1 (return ,combine: combinator)0 combinator(1 ) { 1 2 + 1 } = 2 combinator(3 ) { 3 + 3 } = 3 combinator(6 ,31);">3 ) { 6 + [1,3]
} = 结合(Combinator) = 累加器(Accumulator) @H_502_144@ Int
中的每个元素都将调用一次Optional<Int>
函数进行处理。同时我们使用reduce
变量实时记录递增状态(递增并非是指加法),这里是一个整型值。
接下来,我们重新实现那些函数式编程的「伙伴」(自己来写 map、flatMap 和 filter 函数)。简便起见,所有这些方法都是对// 重新定义一个 map 函数 或rmap 进行 *** 作的;换言之,我们此刻不考虑泛型。另外牢记下面的实现只是为了展示(Int) 的实现过程。原生的 Swift 实现相比较下面 reduce 的版本,速度要快很多1。不过,Reduce 能在不同的问题中表现得很好,之后会进一步地详述。
Map Int Int (elements: [Int],transform: return -> reduce ) -> [Int ] { var elements.Int ([Int ](),combine: { (Int acc: [in ],obj: acc.append(transform(obj)) ) -> [return ] }) } print acc 4 0 2 (rmap([// [2,4,6,8] ],transform: { $reduce
* elements.reduce...
}))[Int]()
@H_502_144@ 这个例子能够很好地帮助你理解combinator
的基础知识。
首先,elements 序列调用 reduce 方法:acc: [Int]
。 然后,我们传入初始值给累加器(Accumulator),即一个 Int 类型空数组(obj: Int
)。 接着,我们传入combinator
闭包,它接收两个参数:第一个参数为 accumulator,即map
;第二个参数为从序列中取得的当前对象reduce
(译者注:对序列进行遍历,每次取到其中的一个对象 obj)。 Int 闭包体中的实现代码非常简单。我们对 obj 做变换处理,然后添加到累加器 accumulator 中。最后返回 accumulator 对象。 相比较调用Int 方法,这种实现代码看起来有点冗余。的确如此!但是,上面这个版本相当详细地解释了// 0 表示第一个传入参数, 表示第二个传入参数,依次类推... 方法是怎么工作的。我们可以对此进行简化。
(Int) -> 1 ) -> [} ] { +
这里有个地方需要引起注意: + [transform($Int )]})
Int 依旧能够正常运行。这个版本都有哪些不同呢?实际上,我们使用了 Swift 中的小技巧,Int 运算符能够对两个序列进行加法 *** 作。因此in 。倘若你正在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增: var ac: [// 作者提倡使用这种,因为执行速度更快 ],b: ac.append(transform(b)) ) -> [return ] }) } reduce
filter
ac (Int) Int @H_502_144@ 为了进一步加深对Int 的理解,我们将继续重新实现guard 方法。
rflatMaplet -> 1 ?) -> [else ] { combine: { return 0 m = transform($0 ) } { print $guard } 0 + [m]}) 3 return (rflatMap([nil $guard
!= filter (Int) Bool }; 这里 rflatMap 和 rmap 主要差异在于,前者增加了一个Int 表达式确保可选类型始终有值(换言之,摒弃那些 nil 的情况)。 Filter rFilter guard : filter -> 0 ) -> [1 ] { } print ($4 + [$filter ]}) 0 2 (rFilter([0 ,31);">6 ],// [4,6] : { $reduce
% map
== filter
}))4 @H_502_144@ 依旧难度不大。我们再次使用 guard 表达式确保满足筛选条件。
到目前为止,// 10 方法看起来更像是+
或combinator
的复杂版本,除此之外然并卵。不过,所结合的内容不需要是一个数组,它可以是其他任何类型。这使得我们依靠一种简单的方式,就可以轻松地实现各种 reduction *** 作。
Reduce 范例 首先介绍我最喜欢的数组元素求和范例:
// 初始值 initial 为 0,每次遍历数组元素,执行 + *** 作[lhs(left-hand sIDe,等式左侧)
].rhs(Right-hand sIDe,等式右侧)
@H_502_144@ 仅传入reduce
作为一个// 24 函数是有效的,它仅仅是对5 和1 做加法处理,最后返回结果值,这完全满足0 函数的要求。
另外一个范例:通过一组数字计算他们的乘积:
// 初始值 initial 为 1,每次遍历数组元素,执行 * *** 作[// 5,3,1 @H_502_144@ 甚至我们可以反转数组:
// $0 指累加器(accumulator),$1 指遍历数组得到的一个元素[typealias ].Acc ] + $Int }) Int @H_502_144@ 最后,来点有难度的任务。我们想要基于某个标准对列表做划分(Partition)处理:
// 为元组定义个别名,此外 Acc 也是闭包传入的 accumulator 的类型partition (Int) = (l: [Bool ],r: [Acc ])return (lst: [Int],criteria: reduce -> Int ) -> Int { Acc lst.Int ((l: [Acc ](),r: [in ]()),combine: { (ac: if ,o: return ) -> else return } criteria(o) { }) (l: ac.l + [o],r: ac.r) } } { partition (r: ac.r + [o],l: ac.l) 5 //: ([2,8],[1,5,7,9]) tuple
reduce
([tuple
,31);">6 ,31);">7,31);">8,31);">9],criteria: { $map
@H_502_144@ 上面实现中最有意思的莫过于我们使用filter
作为 accumulator。你会渐渐发现,一旦你尝试将map 进入到日常工作流中,3 是一个不错的选择,它能够将数据与 reduce *** 作快速挂钩起来。
执行效率对比:Reduce vs. 链式结构 reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,filter 和0 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种 *** 作往往伴随着性能损失,比如以下代码:
[reduce
({ $Int }).Int ({ $Int }). @H_502_144@ 除了毫无意义之外,它还浪费了 cpu 周期。初始序列(即 [0,4])被重复访问了三次之多。首先是 map,接着 filter,最后对数组内容求和。其实,所有这一切 *** 作我们能够使用in 完全替换实现,极大提高执行效率:
// 这里只需要遍历 1 次序列足矣[if ,r: 3 ) -> 0 return 3 (r + else ) % return { } ac + r + }) } var { 0 ac for in @H_502_144@ 这里给出一个快速的基准运行测试,使用以上两个版本以及 for-loop 方式对一个容量为 100000 的列表做处理 *** 作:
// for-loop 版本Array ux = 0 100000 i if 0 (3 ... } ) { } (i + 测试结果 { ux += (i + reduce
) for-loop
reduce
@H_502_144@ Array
正如你所看见的,100000 版本的执行效率和reverse *** 作非常相近,且是链式 *** 作的一半时间。
不过,在某些情况中,链式 *** 作是优于prefix 的。思考如下范例:
3 (// 0.027 Seconds ).Int ().Int (in )0 @H_502_144@ reduce([],r: return ) -> [// 2.927 Seconds ] accumulator ac.insert(r + ac
) reduce
ac }).reduce
@H_502_144@ 这里,注意到使用链式 *** 作花费 0.027s,这与 reduce *** 作的 2.927s 形成了鲜明的反差,这究竟是怎么回事呢?@L_404_3@
Reddit 网站的搜索结果指出,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 参数
func infoFromState(state state: String,persons: [[String: AnyObject]]) -> (count: Int,age: float) { // 在函数内定义别名让函数更加简洁 typealias Acc = (count: Int,age: float) // reduce 结果暂存为临时的变量 let u = persons.reduce((count: 0,age: 0.0)) { (ac: Acc,p) -> Acc in // 获取地区和年龄 guard let personState = p["city"],personAge = p["age"] // 确保选出来的是来自正确的洲 where personState.hasSuffix(state) // 如果缺失年龄或者地区,又或者上者比较结果不等,返回 else { return ac } // 最终累加计算人数和年龄 return (count: ac.count + 1,age: ac.age + float(personAge)) } // 我们的结果就是上面的人数和除以人数后的平均年龄 return (age: u.age / float(u.count),count: u.count)}print(infoFromState(state: "CA",persons: persons))// prints: (count: 3,age: 34.3333) 将为 0…100000 范围内的每个元素都执行一次复制 *** 作。有关对此更好、更详细的解释请看这篇Airspeedvelocity博客文章。 因此,当我们试图使用tuple
来替换掉一组 *** 作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。
现在,可以回到我们的初始问题:计算人口总数和平均年龄。请试着用作为 accumulator 记录状态值。除此之外,代码读起来简明易懂。 来解决吧。
再一次尝试来写 infoFromState 函数 Acc typealias Acc = (count: Int,age: float)
和早前的范例一样,我们再次使用了reduce
亦或 同时,我们在函数体中定义了一个别名reduce
:// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素 ,起到了简化类型注释的作用。
总结 本文是对// 倘若当前元素小于初始值,初始值就会替换成当前元素 方法的一个简短概述。倘若你不想将过多函数式方法通过链式结构串联起来调用,// 示意写法: initial = min(initial,elem) 是数据的输出形式与传入数据的形式不一致时,reduce 就相当有用了。最后,我将向你展示通过使用 reduce 的各种范例来结束本文,希望能为你带来些许灵感。
更多范例 以下范例展示了9 的其他使用案例。请记住例子只作为展示教学使用,即它们更多地强调 reduce 的使用方式,而非为你的代码库提供通用的解决方法。大多数范例都可以通过其他更好、更快的方式来编写(即通过 extension 或 generics)。并且这些实现方式已经在许多 Swift 库中都有实现,诸如SwiftSequence以及Dollar.swift
Minimum 返回列表中的最小项。显然,// 初始值为 Int.max,传入闭包为 min:求两个数的最小值Int max min [集合(Set)
,31);">reduce (Int .if ,combine: contains ) @H_502_144@ Unique 剔除列表中重复的元素。当然,最好的解决方式是使用return 。
7].else ],168);">inreturn a.} (b) { }) a } // prints: 1,7 { Hashable
a + [b] <T,H: Hashable> (T) H @H_502_144@ Group By 遍历整个列表,通过一个鉴别函数对列表中元素进行分组,将分组后的列表作为结果值返回。问题中的鉴别函数返回值类型需要遵循H 协议,这样我们才能拥有不同的键值。此外保留元素的排序,而组内元素排序则不一定被保留下来。
groupbyT (items: [T],f: return -> reduce ) -> [H : [T ]] { T items.H ([:],168);">var ac: [T : [in ]],o: // o 为遍历序列的当前元素 ) -> [let : [// 通过 f 函数得到 o 对应的键值 ]] if var c h = f(o) // 说明 o 对应的键值已经存在,只需要更新键值对应的数组元素即可 c c else = ac[h] { // 说明 o 对应的键值不存在,需要为字典新增一个键值,对应值为 [o] ac.updateValue([o],forKey: h) .append(o) ac.updateValue( } ,forKey: h) } return { }) } print 10 ac 3 // prints: [2: [2,8,11],0: [3,12],1: [1,10]] print (groupby(["Carl" ,31);">11 ,31);">12],f: { $"Cozy" }))// prints: ["C" : ["Carl","Cozy"],"B" : ["Bethlehem","Belem","Brand"],"Z" : ["Zara"]] items
(groupby([count
,element
,0);">"Bethlehem" ,0);">"Belem",0);">"Brand",0);">"Zara"],31);">0.characters.first! }))<T> @H_502_144@ Interpose 函数给定一个count 数组,每隔1 个元素插入T 元素,返回结果值。下面的实现确保了 element 仅在中间插入,而不会添加到数组尾部。
interpose// cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1 typealias : Int = Acc ) -> [T ] { Int Int reduce = (ac: [1 ],cur: Acc ,cnt: T ) Acc ((ac: [],cur: in ),combine: { (a: switch ,o: // 此时遍历的当前元素为序列中的最后一个元素 ) -> case let where a { 1 count return (ac,cur,31);">_ ) // 满足插入条件 (cur+c ) == items.where : c (ac + [o],31);">0) count return ) 1 // 执行下一步 == c : c (ac + [o,element],cur + 1 ) } }).ac ): } + print ) 5 9 // : [1,5] count (interpose([2 ],element: <T> ))(List1: [T],List2: [T]) T : // Zip2Sequence 返回 [(List1,List2)] 是一个数组,类型为元组 )) @H_502_144@ @H_583_1404@Interdig 该函数允许你有选择从两个序列中挑选元素合并成为一个新序列返回。
interdig// 也就解释了为什么 combinator 闭包的类型是 (ac: [T],o: (T,T)) -> [T] return -> [Zip2Sequence ] { T T T T (List1,List2).in ],o: (return ,1 )) -> [ }) ] } print ac + [o.6 ] n
<T> (List: [T],length: Int) (interdig([T ])) @H_502_144@ Chunk 该函数返回原数组分解成长度为typealias 后的多个数组:
chunkAcc T -> [[T ]] { reduce if = (stack: [[return ]],cur: [else ],168);">let l = List.return ((stack: [],cur: [],31);">0),168);">in 1 ac.cnt == length { } (stack: ac.stack + [ac.cur],cur: [o],31);">1 ) } }) { return (stack: ac.stack,cur: ac.cur + [o],cnt: ac.cnt + } ) print 7 // : [[1,2],[3,4],[5,6],[7]] l.stack + [l.cur] accumulator
(chunk([],length: @H_502_144@ 函数中使用一个更为复杂的,包含了 stack、current List 以及 count 。
译者注:有关 Reduce 底层实现,请看这篇文章。
原文http://swift.gg/2015/12/10/reduce-all-the-things/
我修改了原文一些由于swift版本变化二导致的一些错误代码,现在能兼容swift2.2
总结 以上是内存溢出为你收集整理的Swift化零为整 :Reduce 详解 全部内容,希望文章能够帮你解决Swift化零为整:Reduce 详解所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
赞
(0)
打赏
微信扫一扫
支付宝扫一扫