swift中map与flatMap的用法与研究

mapflatMap是swift中两个高阶函数,用处很大,其实不仅仅是这两个函数,其他的譬如reducefilter等等,都为我们提供了很多功能,是之前在oc里无法提供,或者需要自己去实现的。当然这些函数在其他语言里是一直存在的,譬如Haskell。所以swift是一门多编程范式的语言,这里只是记录一下mapflatMap的用法,以及通过这个,我们来看一下函数式思想在swfit中的运用。

map和flatMap用法

我们直接看例子:

例1 数组中使用

1
2
3
var arr = [1,2,3]
let arr2 = arr.map {$0 + 1}//[2,3,4],将数组中每一个整数加一然后返回,返回值类型是Array
let arr3 = arr.flatMap {[$0 - 1, $0 + 1]}//[0, 2, 1, 3, 2, 4],将数组中每一个整型元素映射成了一个数组,如果用map将会得到一个二维数组,所以用flatmap降维成一维数组

例2 数组的flatMap(在swift4.1中,已经改名为compactMap,下面的源码也贴了出来)还有另外一种用法

1
2
3
var brr = ["1", "2","三", "4"]
//let brr2 = brr.compactMap {Int($0)}
let brr2 = brr.flatMap {Int($0)}//[1, 2, 4],Int()函数将String转成Int类型,有可能成功,也有可能失败,所以返回值类型是Optional<Int>,很明显这边数组brr里的"三"是无法成功转换成整型3的,所以返回nil,结果是[1,2,nil,4],此时的flatMap可以过滤掉了nil

因为这个flatMap除了 降维之外其实还有 filter 的作用,在使用时容易产生歧义,所以社区认为最好把这个flatMap重新拆分出来,使用一个新的方法命名,所以就有了compactMap

例3 optional中使用

1
2
3
var str: String? = "12"
let count = str.map {$0.count}//count = 2,如果这个str有值,就计算这个string的字符个数,如果没有值,就返回nil,注意count的类型为Optional<Int>
let value = str.flatMap {Int($0)}//value = 12,和上面数组中使用一样,Int()函数将String转成Int类型,有可能成功,也有可能失败,所以返回值类型是Optional<Int>,此时如果使用map,那么value返回的类型会变成Optional<Optional<Int>>,嵌套的optional,所以使用flatMap,其实也是降维的概念,使其返回类型变为Optional<Int>。

map和flatMap在标准库中的实现

没有什么比看源码更能直接了当的了解一个类或者方法了,大家也可以直接去swift源码查看。我们会发现,sequenceoptional都实现了这两个方法,如下所示:

sequencemapflatMap 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] {
let initialCapacity = underestimatedCount
var result = ContiguousArray<T>()
result.reserveCapacity(initialCapacity)

var iterator = self.makeIterator()

// Add elements up to the initial capacity without checking for regrowth.
for _ in 0..<initialCapacity {
result.append(try transform(iterator.next()!))
}
// Add remaining elements, if any.
while let element = iterator.next() {
result.append(try transform(element))
}
return Array(result)
}

public func flatMap<SegmentOfResult : Sequence>(
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}

public func flatMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}

public func _compactMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
var result: [ElementOfResult] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}

optionalmapflatMap的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U? {
switch self {
case .some(let y):
return .some(try transform(y))
case .none:
return .none
}
}

public func flatMap<U>(
_ transform: (Wrapped) throws -> U?
) rethrows -> U? {
switch self {
case .some(let y):
return try transform(y)
case .none:
return .none
}
}

看完源码实现,大家就能够明白上面的用法了,这里就不赘述了,不过我们需要透过现象看本质,看一看Apple到底为什么要这样来设计,他的思想是什么。

所以,我们来捋一捋,去掉各种不重要的符号,以及sequence中第二个版本的flatMap已经更名为compactMap,表意更清晰,所以也不会混淆了,所以也暂时去掉他,那么sequenceoptional中的定义如下:

sequence:

1
2
3
func map<T>(transform: (Element) -> T) -> [T]

func flatMap<SegmentOfResult : Sequence>(transform: (Element) -> SegmentOfResult) -> [SegmentOfResult.Element]

optional:

1
2
3
func map<U>(transform: (Wrapped) -> U) -> U?

func flatMap<U>(transform: (Wrapped) -> U?) -> U?

我们会发现,sequencemapflatMap方法的区别主要在于transform方法,一个将element转换为T类型,一个将element转换为sequence类型

同理,optionalmapflatMap方法的区别主要也在于transform方法,一个将解包后的值转换为U类型,一个将解包后的值转换为U?类型。

为什么sequenceoptional都有这两个方法,有什么关联吗,这个需要我们来了解一下函数式编程一些知识。

函数式编程

其实很难给出函数式的准确定义,不过有几个概念是函数式编程中经常会遇到的,FunctorApplicativeMonad,之前对这个了解也不是很深,直到后面看到一篇文章,讲的很通俗易懂,想看小伙伴可以点击这里来查看原文。

封装值

其实这里有一个封装值的概念,我们在编程的时候用到很多基础类型值,譬如3就是一个整型,但是包装值的概念是将整型3放入其中,将其封装起来,对外表现出的类型已经不是整型,可以是其他类型,swift中有一个绝佳的例子enum的Associated Value 如下:

1
2
3
4
5
6
enum Result<T> {
case success(T)
case fail
}

let result = Result.success(3)

如上所示,整型3被封装了起来,对外的类型是enum,这就是封装值的概念,下面的诸多例子我们都会用这个Result来演示。

其实optional就是用enum来实现的如下:

1
2
3
4
enum Optional<T> {
case None
case Some(T)
}

所以大家该明白了,optional就是一种封装值。

Functor

我们平时写代码用的最多的函数是处理普通值函数的代码,譬如如下:

1
2
3
4
5
6
7
let a = 1

func addOne(_ number: Int) -> Int {
return number + 1
}

let b = addOne(a)//b = 2

但是,如果我们把上述的整型a变成封装值,上述的addOne()函数就无法工作了,如下:

1
2
3
4
5
6
7
let a = Result.success(1)//a已经变成了一个封装值

func addOne(_ number: Int) -> Int {
return number + 1
}

let b = addOne(a)//报错,提示a类型不是Int类型

这时候,我们就需要将封装值从它封装的类型Result里拿出来,再传给addOne()函数,才能正常工作,然后将通过addOne()函数计算好后得到的普通结果值再次封装在Result里,返回这个封装结果值,这个过程就是Functor.

我们来实现这个Functor,如下:

1
2
3
4
5
6
7
8
9
10
func functor(_ result: Result<Int>, transform: (Int)-> Int) -> Result<Int> {
switch result {
case let .success(value):
return .success(transform(value))
case .fail:
return .fail
}
}

let b = functor(a, transform: addOne)//b = .success(2)

如果大家把这个functor函数和上面源码中sequenceoptionalmap方法的实现比照,就会发现,sequenceoptional中的map方法就是Functor思想在swift中的实现,有同学会说sequence中的map好像不是啊,其实如果把数组也看作一种封装值,那就是一样的啦。

5b349e05a1db4

Monad

继续上面的例子,如果我们不仅仅a变成封装值,我们的addOne()函数的返回值不再是Int,而是一个封装值,譬如Result<Int>,如下:

1
2
3
func addOne(_ number: Int) -> Result<Int> {
return Result.success(number + 1)
}

那整个计算过程就需要少许改变一下了,因为我们现在的addOne()函数也返回封装值,如果在用刚刚的functor函数,那返回值就会出现封装值里面嵌套封装值,啊,这绝对是我们不想看到的,我们来修改一下functor函数,取个新函数名monad如下:

1
2
3
4
5
6
7
8
9
10
func monad(_ result: Result<Int>, transform: (Int)-> Result<Int>) -> Result<Int> {
switch result {
case let .success(value):
return transform(value)
case .fail:
return .fail
}
}

let c = monad(a, transform: addOne)//c = .success(2)

如果大家把这个monad函数和上面源码中sequenceoptionalflatMap方法的实现比照,就会发现,sequenceoptional中的flatMap方法就是Monad思想在swift中的实现。

但是Monad绝对不仅仅是降维这么简单,它真正厉害的地方是可以将多个函数串联起来,将原来一个完整的流程拆分为多个函数的串联,每个函数完成一个单独的功能,串联起来实现一个复杂的功能,我觉得Monad不单单是个函数,更多的是一种思想,一种解决问题的思路。

我们看一个用这个思想解决的一个问题。

用函数式思想解决网络请求问题

大家经常遇到这样的需求,判断有没有网络->获取接口数据成功与否->解析数据成功与否->保存数据成功与否,为了演示,我写了几个函数如下方便大家理解:

1
2
3
4
func checkNet() -> Result<Bool>//检查网络
func fetchData(canFetch: Bool) -> Result<Data>//获取数据
func parseData(data: Data) -> Result<Dictionary>//解析数据
func saveData(diationary: Dictionary) -> Result<String>//保存数据

我们以前常用的做法是if判断,肯定没问题,但是如果我们尝试用上面提到的函数式思想去考虑的话也许就有更好的方法。

下面是我自己写的常用的一个用以处理结果的类,并将mapflatMap的操作自定义成一个操作符,方便串连调用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import Foundation

///This enumuration describes the result of all situation.
enum Result<T> {
case success(T)
case failure(Error)
}

extension Result {
///Functor
func map<U>(_ transform: (T) -> U) -> Result<U> {
switch self {
case .success(let v):
return .success(transform(v))
case .failure(let error):
return .failure(error)
}
}

///Applicative
func apply<U>(_ transform: Result<(T) -> U>) -> Result<U> {
switch transform {
case .success(let function):
return self.map(function)
case .failure(let error):
return .failure(error)
}
}

///Monad
func flatMap<U>(_ transform: (T) -> Result<U>) -> Result<U> {
switch self {
case .success(let v):
return transform(v)
case .failure(let e):
return .failure(e)
}
}
}

precedencegroup ChainingPrecedence {
associativity: left
higherThan: TernaryPrecedence
}

///Functor
infix operator <^>: ChainingPrecedence

func <^><T, U>(lhs: (T) -> U, rhs: Result<T>) -> Result<U> {
return rhs.map(lhs)
}

///Applicative
infix operator <*>: ChainingPrecedence

func <*><T, U>(lhs: Result<(T) -> U>, rhs: Result<T>) -> Result<U> {
return rhs.apply(lhs)
}

///Monad
infix operator >>-: ChainingPrecedence

func >>-<T, U>(lhs: Result<T>, rhs: (T) -> Result<U>) -> Result<U> {
return lhs.flatMap(rhs)
}

那这个问题可以简单的这样处理 ,如下:

1
let result = checkNet()>>-fetchData>>-parseData>>-saveData

哪一个步骤出错了,错误信息也都能捕捉到,所以这样子是不是就是简单了好多,代码优雅了好多。

总结

swift 是一门多编程范式语言,对于函数式编程的思想集成的很好,标准库已经实现了很多高阶函数,FP思想很多时候能在一些问题的处理上带给我们不同的思路和解决方法,希望以上的信息能够帮到你,谢谢。

Author: Uncle Peter
Link: http://yoursite.com/2018/02/26/swift中map与flatMap的用法与研究/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.