Scala的map函数

  categories:资料  author:

map函数:
函数式编程都有一个map函数,map函数就像一个加工厂,传入一个函数,利用这个函数将集合里的每一个元素处理并将结果返回。
aList.map(processFunc)//就这么简单,aList中的每一个元素将会变成processFunc的返回值。 这个processFunc一般都是匿名函数,因为用过一次后就用不到了。
val l = List(1,2,3) var ll = l.map(x => x*x)//返回 ll=(1,4,9)
函数式编程不考虑循环,直接考虑将函数 应用到(apply) 数据/函数上。 函数编程语言一般都提供了强大的模式匹配的功能,其实就是对应面向对象的if判断或者是switch判断,但是比前两者都要简洁的多。
还有一个函数flatMap,
将map和flatMap说成Scala函数机制的核心都不为过分。
“flatMap “函数的一半功能和map函数一样,不过有个要求,传入的函数在处理完后返回值必须是List, 如果返回值不是List,那么将出错。也就是说,传入的函数是有要求的——返回值用List()函数构造成list才行。 这样,每个元素处理后返回一个List,我们得到一个包含List元素的List,flatMap自动将所有的内部list的元素取出来构成一个List返回。 sample:
var li= List(1,2,3,4) li.flatMap(x => x match { case 3 => List(3.1,3.2) case _ => x*2 })
//结果=>编译错误
main.scala:4: error: type mismatch; found : Int required: scala.collection.GenTraversableOnce[?] case _ => x*2
传入flatMpa的函数返回值必须是List,将x*2改成List(x*2),并且和map函数对比一下,看结果:
var li= List(1,2,3,4) var res = li.flatMap(x => x match { case 3 => List(3.1,3.2) case _ => List(x*2) }) println(res) li= List(1,2,3,4) var res2 = li.map(x => x match { case 3 => List(3.1,3.2) case _ => x*2 }) println(res2)
//=>
List(2, 4, 3.1, 3.2, 8) List(2, 4, List(3.1, 3.2), 8) Program exited.
注意,这里使用了大量的类型推断,li res res2都是利用类型推断的。如果不定义res2而是直接试图覆盖res,将得到类型错误。

——————-

Scala 中的foreach和map方法比较
Scala中的集合对象都有foreach和map两个方法。两个方法的共同点在于:都是用于遍历集合对象,并对每一项执行指定的方法。而两者的差异在于:foreach无返回值(准确说返回void),map返回集合对象。见如下代码及运行结果:b.getClass 得到的是void, 而c.getClass得到的是colletion 。再看代码的第9-11行,foreach和map的运行结果一致。结论就是:foreach 无法代替map. 而map方法却可以代替foreach。

问题:为什么scala提供foreach和map两个方法呢?本人看法是scala做为一种支持函数式编程范式的语言,必然要引入一种机制以支持数学中函数概念,而在数学中函数就是映射,所以scala中有map方法一点都不奇怪。而foreach只是用在不需要对集合执行映射操作,但需要遍历集合时才用到。总而言之,foreach用于遍历集合,而map用于映射(转换)集合到另一个集合。

1 object arrayTest extends App{
2 var increase=(x:Int)=>x+1
3 val someNumbers = List ( -11, -10, – 5, 0, 5, 10)
4 var b = someNumbers.foreach(increase)
5 println(b.getClass)
6 var c = someNumbers.map(increase)
7 println(c.getClass)
8
9 c.foreach((x:Int)=>print(x+” “))
10 println()
11 c.map((x:Int)=>print(x+” “))
12
13 }
运行结果:

—————-

在Scala中存在好几个Zip相关的函数,比如zip,zipAll,zipped 以及zipWithIndex等等。我们在代码中也经常看到这样的函数,这篇文章主要介绍一下这些函数的区别以及使用。
1、zip函数将传进来的两个参数中相应位置上的元素组成一个pair数组。如果其中一个参数元素比较长,那么多余的参数会被删掉。看下英文介绍吧:

Returns a list formed from this list and another iterable collection by combining corresponding elements in pairs. If one of the two collections is longer than the other, its remaining elements are ignored.
scala> val numbers = Seq(0, 1, 2, 3, 4)
numbers: Seq[Int] = List(0, 1, 2, 3, 4)

scala> val series = Seq(0, 1, 1, 2, 3)
series: Seq[Int] = List(0, 1, 1, 2, 3)

scala> numbers zip series
res24: Seq[(Int, Int)] = List((0,0), (1,1), (2,1), (3,2), (4,3))
2、zipAll 函数和上面的zip函数类似,但是如果其中一个元素个数比较少,那么将用默认的元素填充。

The zipAll method generates an iterable of pairs of corresponding elements from xs and ys, where the shorter sequence is extended to match the longer one by appending elements x or y
/**
* User: 过往记忆
* Date: 14-12-17
* Time: 上午10:16
* bolg: https://www.iteblog.com
* 本文地址:https://www.iteblog.com/archives/1225
* 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货
* 过往记忆博客微信公共帐号:iteblog_hadoop
*/
scala> val xs = List(1, 2, 3)
xs: List[Int] = List(1, 2, 3)

scala> val ys = List(‘a’, ‘b’)
ys: List[Char] = List(a, b)

scala> val zs = List(“I”, “II”, “III”, “IV”)
zs: List1 = List(I, II, III, IV)

scala> val x = 0
x: Int = 0

scala> val y = ‘_’
y: Char = _

scala> val z = “_”
z: java.lang.String = _

scala> xs.zipAll(ys, x, y)
res30: List[(Int, Char)] = List((1,a), (2,b), (3,_))

scala> xs.zipAll(zs, x, z)
res31: List[(Int, java.lang.String)] = List((1,I), (2,II), (3,III), (0,IV))
3、zipped函数,这个不好翻译,自己看英文解释吧

The zipped method on tuples generalizes several common operations to work on multiple lists.
scala> val values = List.range(1, 5)
values: List[Int] = List(1, 2, 3, 4)

scala> (values, values).zipped toMap
res34: scala.collection.immutable.Map[Int,Int] = Map(1 -> 1, 2 -> 2, 3 -> 3, 4 -> 4)

scala> val sumOfSquares = (values, values).zipped map (_ * _) sum
sumOfSquares: Int = 30
4、zipWithIndex函数将元素和其所在的下表组成一个pair。

The zipWithIndex method pairs every element of a list with the position where it appears in the list.
scala> val series = Seq(0, 1, 1, 2, 3, 5, 8, 13)
series: Seq[Int] = List(0, 1, 1, 2, 3, 5, 8, 13)

scala> series.zipWithIndex
res35: Seq[(Int, Int)] = List((0,0), (1,1), (1,2), (2,3), (3,4), (5,5), (8,6), (13,7))
5、unzip函数可以将一个元组的列表转变成一个列表的元组

The unzip method changes back a list of tuples to a tuple of lists.
scala> val seriesIn = Seq(0, 1, 1, 2, 3, 5, 8, 13)
seriesIn: Seq[Int] = List(0, 1, 1, 2, 3, 5, 8, 13)

scala> val fibonacci = seriesIn.zipWithIndex
fibonacci: Seq[(Int, Int)] = List((0,0), (1,1), (1,2), (2,3), (3,4), (5,5), (8,6), (13,7))

scala> fibonacci.unzip
res46: (Seq[Int], Seq[Int]) = (List(0, 1, 1, 2, 3, 5, 8, 13),List(0, 1, 2, 3, 4, 5, 6, 7))

scala> val seriesOut = fibonacci.unzip _1
seriesOut: Seq[Int] = List(0, 1, 1, 2, 3, 5, 8, 13)

scala> val numbersOut = fibonacci.unzip _2
numbersOut: Seq[Int] = List(0, 1, 2, 3, 4, 5, 6, 7)

———–
Scala中的map与collect在Scala中,当我需要对集合的元素进行转换时,自然而然会使用到map方法。而当我们在对tuple类型的集合或者针对Map进行map操作时,通常更倾向于在map方法中使用case语句,这比直接使用_1与_2更加可读。例如:

val languageToCount = Map(“Scala” -> 10, “Java” -> 20, “Ruby” -> 5)
languageToCount map { case (_, count) => count + 1 }
然而对于上述场景,其实我们也可以使用collect方法:

languageToCount collect { case (_, count) => count + 1 }
效果完全相同。

我很少在项目中调用collect方法,且这个方法命名的意图也不是特别明显,至少在函数式编程的语境下,map似乎更为通用。今天在阅读Neal Ford的Functional Thinking时,看到书中给出的这样一个案例:

List(1, 3, 5, “seven”) map { case i: Int => i + 1 } //won’t work
//scala.MatchError: seven (of class java.lang.String)
List(1, 3, 5, “seven”) collect { case i: Int => i + 1 } //it works
为什么同样的case语句,放在collect中是正确的,放在map中就会抛出MatchError错误呢?我这才想到去查阅Scala的API文档,发现这个两个函数的定义存在本质上的区别:

def map[B](f: (A) ⇒ B): List[B]

def collect[B](pf: PartialFunction[A, B]): List[B]
两个方法的定义如出一辙,区别就在于前者接收的是一个函数类型的参数,而后者接收的是一个偏函数(partial function)类型的参数:

map: Builds a new collection by applying a function to all elements of this list.

colect: Builds a new collection by applying a partial function to all elements of this list on which the function is defined.

case语句其实是偏函数定义的语法糖,当我们编写一个case语句时,其实等同于创建了一个具有apply与isDefineAt方法的偏函数对象。由于偏函数实质是函数的一种实例,因此可以将case语句传递给map方法,但它此时扮演的是一个普通的匿名函数,而非偏函数。因此,map方法在调用该函数对象的apply方法之前,并没有调用isDefineAt方法判断参数值是否定义。

我们可以对比map方法和collect方法的实现:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
def builder = {
val b = bf(repr)
b.sizeHint(this)
b
}
val b = builder
for (x <- this) b += f(x)
b.result
}

def collect[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
for (x <- this) if (pf.isDefinedAt(x)) b += pf(x)
b.result
}
在调用map方法时,一旦遍历到值”seven”,并调用f(x),因为类型不符合模式匹配中的Int类型,导致抛出MatchError错误。而collect方法在调用pf(x)之前,调用了pf的isDefinedAt(x)作了一次过滤。

如果在前面的map例子中再增加一个case子句,对String类型的值进行处理,则case语句就从偏函数变成了满足所有条件的“全”函数:

List(1, 3, 5, “seven”) map {
case i: Int => i + 1
case s: String => s.length
}
得到的结果为:

List[Int] = List(2, 4, 6, 5)
由于collect方法接收的是一个偏函数类型,所以它并不能接收一个lambda表达式:

List(1, 3, 5, “seven”).collect(i => i + 1)
会抛出:

error: missing parameter type
补充:

我们在使用collect时,可以利用偏函数的原理,同时实现filter与map的特性。例如:

List(1, 2, 3, 4, 5, 6) collect { case i if i % 2 == 0 => i * i }
这段代码相当于:

List(1, 2, 3, 4, 5, 6).filter(i => i % 2 == 0).map(i => i * i)
写作是兴趣,阅读是分享,打赏是鼓励

 

———————–

map & flatMap 浅析

我之前一直以为我是懂 map 和 flatMap 的。但是直到我看到别人说:「一个实现了 flatMap 方法的类型其实就是 monad。」我又发现这个熟悉的东西变得陌生起来,本节烧脑体操打算更细致一些介绍 map 和 flatMap,为了下一节介绍 monad 做铺垫。

准备运动:基础知识

数组中的 map 和 flatMap

数组中的 map 对数组元素进行某种规则的转换,例如:

let arr = [1, 2, 4] // arr = [1, 2, 4]

let brr = arr.map { "No." + String($0) } // brr = ["No.1", "No.2", "No.4"] 

而 flatMap 和 map 的差别在哪里呢?我们可以对比一下它们的定义。为了方便阅读,我在删掉了定义中的 @noescape 、throws 和 rethrows 关键字,如果你对这些关键字有疑问,可以查阅上一期的烧脑文章:

extension SequenceType {
    public func map<T>(transform: (Self.Generator.Element) -> T) 
         -> [T]
}

extension SequenceType {
    public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) -> S) 
         -> [S.Generator.Element]
}

extension SequenceType {
    public func flatMap<T>(transform: (Self.Generator.Element) -> T?) 
         -> [T]
}

我们从中可以发现,map 的定义只有一个,而 flatMap 的定义有两个重载的函数,这两个重载的函数都是接受一个闭包作为参数,返回一个数组。但是差别在于,闭包的定义不一样。

第一个函数闭包的定义是:(Self.Generator.Element) -> S,并且这里 S 被定义成:S : SequenceType。所以它是接受数组元素,然后输出一个 SequenceType 类型的元素的闭包。有趣的是, flatMap 最终执行的结果并不是 SequenceType 的数组,而是 SequenceType 内部元素另外组成的数组,即:[S.Generator.Element]

是不是有点晕?看看示例代码就比较清楚了:

let arr = [[1, 2, 3], [6, 5, 4]]
let brr = arr.flatMap {
    $0
}
// brr = [1, 2, 3, 6, 5, 4]

你看出来了吗?在这个例子中,数组 arr 调用 flatMap 时,元素[1, 2, 3] 和 [6, 5, 4] 分别被传入闭包中,又直接被作为结果返回。但是,最终的结果中,却是由这两个数组中的元素共同组成的新数组:[1, 2, 3, 6, 5, 4] 。

需要注意的是,其实整个 flatMap 方法可以拆解成两步:

  • 第一步像 map 方法那样,对元素进行某种规则的转换。
  • 第二步,执行 flatten 方法,将数组中的元素一一取出来,组成一个新数组。

所以,刚刚的代码其实等价于:

let arr = [[1, 2, 3], [6, 5, 4]]
let crr = Array(arr.map{ $0 }.flatten())
// crr = [1, 2, 3, 6, 5, 4]

讲完了 flatMap 的第一种重载的函数,我们再来看第二种重载。

在第二种重载中,闭包的定义变成了:(Self.Generator.Element) -> T?,返回值 T 不再像第一种重载中那样要求是数组了,而变成了一个 Optional 的任意类型。而 flatMap 最终输出的数组结果,其实不是这个 T? 类型,而是这个 T? 类型解包之后,不为 .None 的元数数组:[T]

我们还是直接看代码吧。

let arr: [Int?] = [1, 2, nil, 4, nil, 5] 
let brr = arr.flatMap { $0 } 
// brr = [1, 2, 4, 5]

在这个例子中,flatMap 将数组中的 nil 都丢弃掉了,只保留了非空的值。

在实际业务中,这样的例子还挺常见,比如你想构造一组图片,于是你使用 UIImage 的构造函数,但是这个函数可能会失败(比如图像的名字不存在时),所以返回的是一个 Optional 的 UIImage 对象。使用 flatMap 方法可以方便地将这些对象中为 .None 的都去除掉。如下所示:

let images = (1...6).flatMap {
    UIImage(named: "imageName-\($0)") 
}  

Optional 中的 map 和 flatMap

其实 map 和 flatMap 不止存在于数组中,在 Optional 中也存在。我们先看看定义吧:

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)

    public func map<U>( f: (Wrapped) throws -> U) 
        rethrows -> U?

    public func flatMap<U>( f: (Wrapped) throws -> U?) 
        rethrows -> U?
}

所以,对于一个 Optional 的变量来说,map 方法允许它再次修改自己的值,并且不必关心自己是否为 .None。例如:

let a1: Int? = 3
let b1 = a1.map{ $0 * 2 }
// b1 = 6

let a2: Int? = nil
let b2 = a2.map{ $0 * 2 }
// b2 = nil

再举一个例子,比如我们想把一个字符串转成 NSDate 实例,如果不用 map 方法,我们只能这么写:

var date: NSDate? = ...
var formatted = date == nil ? nil : NSDateFormatter().stringFromDate(date!)

而使用 map 函数后,代码变得更短,更易读:

var date: NSDate? = ...
var formatted = date.map(NSDateFormatter().stringFromDate)

看出来特点了吗?当我们的输入是一个 Optional,同时我们需要在逻辑中处理这个 Optional 是否为 nil,那么就适合用 map 来替代原来的写法,使得代码更加简短。

那什么时候使用 Optional 的 flatMap 方法呢?答案是:当我们的闭包参数有可能返回 nil 的时候。

比如,我们希望将一个字符串转换成 Int,但是转换可能失败,这个时候我们就可以用 flatMap 方法,如下所示:

let s: String? = "abc"
let v = s.flatMap { (a: String) -> Int? in
    return Int(a)
}

我在这里还发现了更多的使用 map 和 flatMap 的例子,分享给大家:http://blog.xebia.com/the-power-of-map-and-flatmap-of-swift-optionals/。

map 和 flatMap 的源码

Talk is cheap. Show me the code.

— Linus Torvalds

为了更好地理解,我们去翻翻苹果开源的 Swift 代码,看看 map 和 flatMap 的实现吧。

数组的 map 的源码

源码地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Collection.swift,摘录如下:

public func map<T>(@noescape transform: (Generator.Element) throws -> T)
        rethrows -> [T] {
    let count: Int = numericCast(self.count)
    if count == 0 {
        return []
    }

    var result = ContiguousArray<T>()
    result.reserveCapacity(count)

    var i = self.startIndex

    for _ in 0..<count {
        result.append(try transform(self[i]))
        i = i.successor()
    }

    _expectEnd(i, self)
    return Array(result)
}

数组的 flatMap 的源码(重载函数一)

刚刚也说到,数组的 flatMap 有两个重载的函数。我们先看第一个的函数实现。源码地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift.gyb。

public func flatMap<S : SequenceType>(
        transform: (${GElement}) throws -> S
    ) rethrows -> [S.${GElement}] {
        var result: [S.${GElement}] = []
        for element in self {
            result.appendContentsOf(try transform(element))
        }
        return result
}

对于这个代码,我们可以看出,它做了以下几件事情:

  1. 构造一个名为 result 的新数组,用于存放结果。
  2. 遍历自己的元素,对于每个元素,调用闭包的转换函数 transform,进行转换。
  3. 将转换的结果,使用 appendContentsOf 方法,将结果放入 result 数组中。

而这个 appendContentsOf 方法,即是把数组中的元素取出来,放入新数组。以下是一个简单示例:

var arr = [1, 3, 2]
arr.appendContentsOf([4, 5])
// arr = [1, 3, 2, 4, 5]

所以这种 flatMap 必须要求 transform 函数返回的是一个 SequenceType 类型,因为appendContentsOf 方法需要的是一个 SequenceType 类型的参数。

数组的 flatMap 的源码(重载函数二)

当我们的闭包参数返回的类型不是 SequenceType 时,就会匹配上第二个重载的 flatMap 函数。以下是函数的源码。

public func flatMap<T>(
    @noescape transform: (${GElement}) throws -> T?
    ) rethrows -> [T] {
        var result: [T] = []
        for element in self {
            if let newElement = try transform(element) {
                result.append(newElement)
            }
        }
        return result
}

我们也用同样的方式,把该函数的逻辑理一下:

  1. 构造一个名为 result 的新数组,用于存放结果。(和另一个重载函数完全一样)
  2. 遍历自己的元素,对于每个元素,调用闭包的转换函数 transform,进行转换。(和另一个重载函数完全一样)
  3. 将转换的结果,判断结果是否是 nil,如果不是,使用使用 append 方法,将结果放入result 数组中。(唯一差别的地方)

所以,该 flatMap 函数可以过滤闭包执行结果为 nil 的情况,仅收集那些转换后非空的结果。

对于这种重载的 flatMap 函数,它和 map 函数的逻辑非常相似,仅仅多做了一个判断是否为 nil 的逻辑。

所以,面试题来了:「什么情况下数组的 map 可以和 flatMap 等价替换?」

答案是:当 map 的闭包函数返回的结果不是 SequenceType 的时候。因为这样的话,flatMap就会调到我们当前讨论的这种重载形式。而这种重载形式和 map 的差异就仅仅在于要不要判断结果为 nil。

下面是一个示例代码,可以看出:brr 和 crr 虽然分别使用 map 和 flatMap 生成,但是结果完全一样:

let arr = [1, 2, 4]
// arr = [1, 2, 4]

let brr = arr.map {
    "No." + String($0)
}
// brr = ["No.1", "No.2", "No.4"]

let crr = arr.flatMap {
    "No." + String($0)
}
// crr = ["No.1", "No.2", "No.4"]

Optional 的 map 和 flatMap 源码

看完数组的实现,我们再来看看 Optional 中的相关实现。源码地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift,摘录如下:

/// If `self == nil`, returns `nil`.  
/// Otherwise, returns `f(self!)`.
public func map<U>(@noescape f: (Wrapped) throws -> U) 
        rethrows -> U? {
    switch self {
    case .Some(let y):
        return .Some(try f(y))
    case .None:
        return .None
    }
}

/// Returns `nil` if `self` is `nil`, 
/// `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) 
        rethrows -> U? {
    switch self {
    case .Some(let y):
        return try f(y)
    case .None:
        return .None
    }
}

Optional 的这两函数真的是惊人的相似,如果你只看两段函数的注释的话,甚至看不出这两个函数的差别。

这两函数实现的差别仅仅只有两处:

  1. f 函数一个返回 U,另一个返回 U? 。
  2. 一个调用的结果直接返回,另一个会把结果放到 .Some 里面返回。

两个函数最终都保证了返回结果是 Optional 的。只是将结果转换成 Optional 的位置不一样。

这就像我老婆给我说:「我喜欢这个东西,你送给我吗?不送的话我就直接刷你卡买了!」。。。买东西的结果本质上是一样的,谁付钱本质上也是一样的,差别只是谁动手而已。

既然 Optional 的 map 和 flatMap 本质上是一样的,为什么要搞两种形式呢?这其实是为了调用者更方便而设计的。调用者提拱的闭包函数,既可以返回 Optional 的结果,也可以返回非 Optional 的结果。对于后者,使用 map 方法,即可以将结果继续转换成 Optional 的。结果是 Optional 的意味着我们可以继续链式调用,也更方便我们处理错误。

我们来看一段略烧脑的代码,它使用了 Optional 的 flatMap 方法:

var arr = [1, 2, 4]
let res = arr.first.flatMap {
    arr.reduce($0, combine: max)
}

这段代码的功能是:计算出数组中的元素最大值,按理说,求最大值直接使用 reduce 方法就可以了。不过有一种特殊情况需要考虑:即数组中的元素个数为 0 的情况,在这种情况下,没有最大值。

我们使用 Optional 的 flatMap 方法来处理了这种情况。arr 的 first 方法返回的结果是 Optional 的,当数组为空的时候,first 方法返回 .None,所以,这段代码可以处理数组元素个数为 0 的情况了。

烧脑的 map 和 flatMap

关于取名

There are only two hard things in Computer Science: cache invalidation and naming things.

— Phil Karlton

有一位大师说,计算机世界真正称得上难题的就只有两个:第一个是缓存过期问题,第二个就是取名字。作为文章最后的烧脑环节,我们来聊聊取名字这个事吧。

我来提几个看起来「无厘头」的问题:

  • 数组的 map 函数和 Optinal 的 map 函数的实现差别巨大?但是为什么都叫 map 这个名字?
  • 数组的 flatMap 函数和 Optinal 的 flatMap 函数的实现差别巨大?但是为什么都叫flatMap 这个名字?
  • 数组的 flatMap 有两个重载的函数,两个重载的函数差别巨大,但是为什么都叫 flatMap 这个名字?

在我看来,这样的取名其实都是有背后的原因的,我试着分享一下我的理解。我们先说结论,然后再解释。这段结论来自:http://www.mokacoding.com/blog/functor-applicative-monads-in-pictures/。

  • 数组和 Optional 的 map 函数都叫一样的名字,是因为它们都是 Functor。
  • 数组和 Optinal 的 flatMap 函数都叫一样的名字,是因为它们都是 Monad。

好吧,我猜你心里开始骂娘了:「为了解释一个问题,引入了两个新问题:谁知道什么是 Functor 和 Monad !」

不要着急,我们先说严谨的结论有助于更好地总结和归纳,我下面试着解释一下 Functor 和 Monad。

Functor

Functor 在 Wikipedia 上的定义非常学术。我想了一个相对比较容易理解的定义:所谓的 Functor,就是可以把一个函数应用于一个「封装过的值」上,得到一个新的「封装过的值」。通常情况下,我们会把这个函数叫做 map

什么叫做「封装过的值」呢?数组就是对值的一种封装,Optional 也是对值的一种封装。如果你愿意,你也可以自己封装一些值,比如把网络请求的结果和网络异常封装在一起,做成一个 enum (如下所示)。

enum Result<T> {
    case Success(T)
    case Failure(ErrorType)
}

一个值能否成为「封装过的值」,取决于这个值的类型所表示的集合,通过 map 函数,能否映射到一个新集合中。这个新集合,也要求能够继续使用 map 函数,再映射到另外一个集合。

我们拿数组和 Optional 类型来检查这个规则,就会发现是符合的:

  • 数组可以通过 map 函数,生成一个新的数组,新的数组可以继续使用 map 函数。
  • Optional 可以通过 map 函数,生成一个新的 Optional 变量,新的 Optional 变量可以继续使用map 函数。

所以,数组 和 Optional 都是 Functor。

Monad

如果你能理解 Functor,那么 Monad 就相对容易一些了。所谓的 Monad,和 Functor 一样,也是把一个函数应用于一个「封装过的值」上,得到一个新的「封装过的值」。不过差别在于:

  • Functor 的函数定义是从「未封装的值」到「未封装的值」的
  • Monad 的函数定义是从「未封装的值」到「封装后的值」的。

下面我举例解释一下:

刚刚我们说,数组 和 Optional 都是 Functor,因为它们支持用 map 函数做「封装过的值」所在集合的变换。那么,你注意到了吗?map 函数的定义中,输入的参数和返回的结果,都不是「封装过的值」,而是「未封装的值」。什么是「未封装的值」?

  • 对于数组来说,「未封装的值」是数组里面一个一个的元素,map 函数的闭包接受的是一个一个的元素,返回的也是一个一个的元素。
  • 对于 Optional 来说,「未封装的值」是 Optional 解包出来的值,map 函数的闭包接受的是解包出来的值,返回的也是解包出来的值。

下面是数组的示例代码,我故意加上了闭包的参数,我们再观察一下。我们可以发现,map 的闭包接受的是 Int 类型,返回的是 String 类型,都是一个一个的元素类型,而不是数组。

// map 的闭包接受的是 Int 类型,返回的是 String 类型,都是一个一个的元素类型,而不是数组。
let arr = [1, 2, 4]
let brr = arr.map {
    (element: Int) -> String in
    "No." + String(element)
}

下面是 Optional 的示例代码,我也故意加上了闭包的参数。我们可以发现,map 的闭包接受的是 Int 类型,返回的是 Int 类型,都是非 Optional 的。

// map 的闭包接受的是 Int 类型,返回的是 Int 类型,都是非 Optional 的。
let tq: Int? = 1
tq.map { (a: Int) -> Int in
    a * 2
}

我们刚刚说,对于 Monad 来说,它和 Functor 的差异实在太小,小到就只有闭包的参数类型不一样。数组实现了 flatMap ,它就是一种 Monad,下面我们就看看 flatMap 在数组中的函数定义,我们可以看出,闭包接受的是数组的元素,返回的是一个数组(封装后的值)。

// 闭包接受的是数组的元素,返回的是一个数组(封装后的值)
let arr = [1, 2, 3]
let brr = arr.flatMap {
    (element:Int) -> [Int] in
    return [element * 2]
}

下面是 flatMap 在 Optional 中的定义,我们可以看出,闭包接受的是 Int 类型,返回的是一个 Optional(封装后的值)。

// 闭包接受的是 Int 类型,返回的是一个 Optional(封装后的值)
let tq: Int? = 1
tq.flatMap { (a: Int) -> Int? in
    if a % 2 == 0 {
        return a
    } else {
        return nil
    }
}

所以本质上,map 和 flatMap 代表着一类行为,我们把这类行为叫做 Functor 和 Monad。它们的差异仅仅在于闭包函数的参数返回类型不一样。所以,我们才会把数组和 Optional 这两个差别很大的类型,都加上两个实现差别很大的函数,但是都取名叫 map 和 flatMap

多重 Optional

我们在第一节烧脑文章中提到过多重 Optional,在使用 map 的时候不仔细,就会触发多重 Optional 的问题。比如下面这个代码,变量 b 因为是一个两层嵌套的 nil,所以 if let 失效了。

let tq: Int? = 1
let b = tq.map { (a: Int) -> Int? in
    if a % 2 == 0 {
        return a
    } else {
        return nil
    }
}
if let _ = b {
    print("not nil")
}

解决办法是把 map 换成 flatMap 即可。

总结

讨论完了,我们总结一下:

  • 数组和 Optional 都能支持 map 和 flatMap 函数。
  • 数组的 flatMap 有两个重载的实现,一个实现等价于先 map 再 flatten,另一个实现用于去掉结果中的 nil。
  • 通过阅读源码,我们更加深入理解了 map 和 flatMap 函数内部的机制。
  • 我们讨论了 map 和 flatMap 的取名问题,最后得出:一个类型如果支持 map,则表示它是一个 Functor;一个类型如果支持 flatMap,则表示它是一个 Monad。
  • 我们讨论了 map 中使用不当造成的多重 Optional 问题。

 

 

转载自:http://www.infoq.com/cn/articles/swift-brain-gym-map-and-flatmap?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=articles_link&utm_content=link_text



快乐成长 每天进步一点点      京ICP备18032580号-1