Swift之函数式编程

最近学习 Swift 的函数式编程,觉得甚是蒙圈,幸好碰到唐巧的一系列烧脑文章,干货满满。这里分五部分记一些重点笔记和个人理解:

铺垫一:Optional 可选

在OC中,向一个 nil 的对象发消息是默认不产生任何效果的行为,但是对于强类型语言 Swift ,需要在编译期做更多的检查,故而引入类型推断,而避免空指针调用是安全的类型推断基本需求之一,于是 Optional 应运而生。

Optional 在 Swift 中实际上是一个枚举类型:

public enum Optional<Wrapped> : ExpressibleByNilLiteral
{
    case none
    case some(Wrapped)

    public init(nilLiteral: ()) { self = .none } // var i: Index? = nil
    public init(_ some: Wrapped) { self = .some(some) } // var i: Int? = Int('42')
}

Optional 类型的变量可以理解为一个薛定谔的包裹,在使用时需要解包,解出来的可能是变量的值,也可能是 nil,也可能是另一个包裹(就像套娃)。

通常我们使用if let方式解包,但是这种方式在套娃模式下就会出现问题。例如:

let a: Int? = nil
let b: Int?? = a
let c: Int?? = nil

if let _ = a {
    print("a is not nil")
}
if let _ = b {
    print("b is not nil")
}
if let _ = c {
    print("c is not nil")
}

运行结果就是:

b is not nil

What??? 说好的 if let 判空怎么不按理出牌了?

使用fr v -R a查看a变量的内存结构。

(Swift.Optional<Swift.Int>) a = none {
  some = {
    _value = 0
  }
}
(Swift.Optional<Swift.Optional<Swift.Int>>) b = some {
  some = none {
    some = {
      _value = 0
    }
  }
}
(Swift.Optional<Swift.Optional<Swift.Int>>) c = none {
  some = some {
    some = {
      _value = 0
    }
  }
}

这个内存结构类似二叉树:

  • 对于一层可选a,它可以是一个.none,也可以是一个Int的数值;
  • 对于二层可选b,它可以是.none,也可以是一个Optional<Int>,这就导致b初始化时传入一个Optional<Int>型的a,它就成为了.some类型,不能被if let判空。
  • 对于二层可选c,它传入了一个nil,就被初始化成了.none类型。

铺垫二:函数

先来看看匿名函数(闭包)的瘦身历程。

闭包表达式的完整形式:

{ (参数) -> 返回值类型 in
    代码
}

以下为例:

func tailingClosures(num: Int, handler: (_ a: Int, _ b: Int) -> Int){
    handler(num * 2, num + 2)
}

普通调用方法👇:

tailingClosures(num: 3, handler: { (a:Int, b:Int) -> Int in
    return a + b
})

当最后一个参数为闭包时,可以使用尾随闭包👇:将闭包置于函数后,并省去参数。

tailingClosures(num: 3) { (a:Int, b:Int) -> Int in
    return a + b
}

如果一个函数的返回类型和参数类型可以推导出来,则返回类型和参数类型可以省略。👇

tailingClosures(num: 3) { (a:Int, b:Int) in
    return a + b
}

tailingClosures(num: 3) { (a, b) in
    return a + b
}

如果参数的个数可以推导出来,可以不写参数。使用$0,$1,$2... 这样的方式引用参数。👇

tailingClosures(num: 3) {
    return $0 + $1
}

如果函数体只有一行,可以把return省略掉。👇

tailingClosures(num: 3) {
    $0 + $1
}

看完了瘦身后的函数表达式,并不意味着结束,看看这个无聊的需求:通过一个构造工厂函数,传入两个函数返回一个新的函数。

func funcBuild(f: @escaping (Int) -> Int, g: @escaping (Int) -> Int)
    -> (Int) -> Int
{
    return { f(g($0)) }
}

let f1 = funcBuild(f: {$0 + 2}, g: {$0 + 3})
f1(0) // 结果为5
let f2 = funcBuild(f: {$0 * 2}, g: {$0 * 5})
f2(1) // 结果为10

这里有一个关键字@escaping,它表示这个闭包是可以“逃出”这个函数的。什么意思呢?当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,这个闭包被称为逃逸闭包。通常会用在异步操作的情况。将一个闭包标记为@escaping意味着必须在逃逸闭包中显示地引用self

另外,上面的->用多了之后就有点儿眼花缭乱了,写了一遍又一遍还不容易扩展,还记得typealias嘛?它能使代码清晰很多。

typealias IntFunction = (Int) -> Int

func funcBuild(f: @escaping IntFunction, g: @escaping IntFunction)
    -> IntFunction
{
    return { f(g($0)) }
}

还有一种更宽泛的写法,那就是使用泛型:

func funcBuild<T, U, V>(f: @escaping (T) -> U, g: @escaping (V) -> T) -> (V) -> U
{
    return { f(g($0)) }
}

let f1 = funcBuild(f: {$0 + 2}, g: {$0 + 3})
let f2 = funcBuild(f: {"NO.\($0)"}, g: {$0 * 10})
f2(2)

高阶函数

先说两个概念型的名词:

高阶函数(high order func),指可以将其他函数作为参数或者返回结果的函数。

一级函数(first class func),指可以出现在任何其他构件(比如变量)地方的函数。

map

map { (Element) -> Element in
    对 element 进行处理
}

一般用在集合类型,对集合里的元素进行遍历,函数体里实现对每一个元素的操作。

var arr = [1,3,2,4]
let mapres = arr.map {
    "NO." + String($0)
}
// 运行结果:["NO.1", "NO.3", "NO.2", "NO.4"]

reduce

reduce(Result) { (Result, Element) -> Result in
    基于 Result 对当前的 Element 进行操作,并返回新的 Result
}

一般用在集合类型,对集合里的元素进行叠加处理,函数体里传两个参数,第一个是之前的叠加结果,第二个是当前元素,返回值是对当前元素叠加后的结果。

// 对数组里的元素:奇数相加,偶数相乘
var arr = [1,3,2,4]
let reduceRes = arr.reduce((0,1)) { (a:(Int,Int), t:Int) -> (Int,Int) in
    if t % 2 == 1 {
        return (a.0 + t, a.1)
    } else {
        return (a.0, a.1 * t)
    }
}
// 运行结果:(4,8)

filter

filter { (Element) -> Bool
    对元素的筛选条件,返回 Bool
}

一般用在集合类型,对集合里的元素进行筛选。函数体里实现筛选条件,返回 true 的元素通过筛选。

var arr = [1,3,2,4]
let filterRes = arr.filter {
    $0 % 2 == 0
}
// 运行结果:[2,4]

flatMap

首先先看下 Swift 源码里对集合数组的mapflatmap的实现:

// Sequence.swift
extension Sequence {
    public func map<T>(_ transform: (Element) -> T) -> [T] {}
}

// SequenceAlgorithms.swift.gyb
extension Sequence {
    public func flatMap<T>(_ transform: (Element) -> T?) -> [T] {}
    public func flatMap<S : Sequence>(_ transform: (Element) -> S) -> [S.Element] {}
}

前面我们已经知道,map是一种遍历,而上面的代码又显示出来,flatmap有两种重载的函数:

  • 其中一种与map非常相似,差别只在闭包里的返回值变成了可选类型。
  • 另一种稍微有点不同,闭包传入的是数组,最后返回的是数组的元素组成的集合。
// map
let arr = [1,2,nil,4,nil,5]
let arrRes = arr.map { $0 } // 结果为:[Optional(1), Optional(2), nil, Optional(4), nil, Optional(5)]

// flatmap
let brr = [1,2,nil,4,nil,5]
let brrRes = brr.flatmap { $0 } // 结果为:[1, 2, 4, 5]

let crr = [[1,2,4],[5,3,2]]
let ccRes = crr.flatmap { $0 } // 结果为:[1, 2, 4, 5, 3, 2]
let cdRes = crr.flatmap { c in
    c.map { $0 * $0 }
} // 结果为[1, 4, 16, 25, 9, 4]

// 使用 map 实现的上面平铺功能
let ceRes = Array(crr.map{ $0 }.joined()) // 同 ccRes
let cfRes = Array(crr.map{ $0 }.joined()).map{ $0 * $0 } // 同 cdRes

简单理解为,flatMap可以将多维数组平铺,也还以过滤掉一维数组中的nil元素。

mapflatMap不只在数组中可以使用,对于 Optional 类型也是可以进行操作的。先看下面这个例子:

let a: Date? = Date()
let formatter = DateFormatter()
formatter.dateStyle = .medium

let c = a.map(formatter.string(from:))
let d = a == nil ? nil : formatter.string(from: a!)

c 和 d 是两种不同的写法,c 写法是不是更优雅一些?

下面我们看一下 Swift 源码中对 Optional 的 mapflatmap实现:

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
    }
}

二者的区别在于闭包里对 Optional 的处理时机:map在拿到解包后的元素后进行操作,操作完之后对元素再次封包,并作为未封包的结果返回;而flatMap会直接拿着处理后的元素作为封包后的结果返回,也就意味着flatMap认为在transform(y)过程中已经进行了封包操作。

具体是什么意思呢?看👇例子的情况:

let s: String? = "abc"
let v = s.flatMap { Int($0) }
let u = s.map { Int($0) }

if let _=v {
    print("v")
}

if let _=u {
    print("u")
}

还记的铺垫一里的多层可选if let判断么?

先说结论,结果会输出u,因为:

v: Int?
u: Int??

具体使用fr v -R查看一下。

(Swift.Optional<Swift.Int>) v = none {
  some = {
    _value = 0
  }
}
(Swift.Optional<Swift.Optional<Swift.Int>>) u = some {
  some = none {
    some = {
      _value = 0
    }
  }
}

再看下面这个flatMap的例子吧:

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

它的功能就是计算数组的元素最大值,而且考虑了数组为空的情况。

在实际使用中呢,如果闭包的返回值必然不为nil,可以使用map的方式自动封装,但是如果闭包里面的处理结果有可能是nil,那么还是使用flatMap来避免产生多层可选的问题吧。

烧脑消食

看看巧哥的这几个问答:

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

因为它们都是Functor。可以理解为:把一个函数应用于一个“封装过的值”上,得到一个新的“封装过的值”,但是函数的定义是从“未封装的值”到 “未封装的值”

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

因为它们都是Monad。可以理解为:把一个函数应用于一个“封装过的值”上,得到一个新的“封装过的值”,但是函数的定义是从“未封装的值”到 “封装后的值”

什么是Functor?什么是Monad呢?我看了一些文章之后,觉得下面这张图最能有效说明:

  • Functor:应用一个函数到封装后的值,如map
  • Applicative:应用一个封装后的函数到封装后的值
  • Monad:应用一个返回封装后的值的函数到一个封装后的值,如flatMap

阅读:


写在最后:本文同步发布在我的个人网站上,欢迎指点。(能看到这里的应该是真爱了吧 : )

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容