Swift的一次函数式之旅

本文首发于搜狐产品技术公众号,如今是同步发布在个人掘金上,你们看完有问题能够留留言讨论讨论。java

本文适合哪些人?

本文针对的是已经有一部分Swift开发的基础,可是对函数式范式比较感兴趣的开发者。固然,若是只对函数式范式感兴趣,我以为这篇文章也值得一看。react

函数式编程是什么?

首先来看这个词语”Functional Programming“,它是什么?程序员

当须要去查一个专业术语的定义的时候,个人第一反应是来查询Wikipedia:spring

In computer science, fucnitonal programming is a programming paradigm where programs are constructed by applying and composing fucntions.编程

在这个定义里,有一个很熟悉的词——programming paradigm, 通常翻译为编程范式,但是我对这个翻译仍是有些迷糊,因而我又在wikipedia中查找这个词语的含义:swift

Programming paradigms are a way to classify programming languages based on their features. 编程范式(编程范例)是一种基于语言自身的特性来给编程语言分类的方式。api

同时wikipedia中还总结了常见的编程范式的分类:markdown

  • imperative
    • procedural
    • object-oriented
  • declarative
    • functional
    • logic
    • mathematical
    • reactive

那么究竟什么是编程范式呢?咱们知道编程是一门工程学,它的目的是去解决问题,而解决问题能够有不少的方法,编程范例就是表明着解决问题的不一样思路。若是说咱们是编程世界的造物主的话,那么编程范例应该就是咱们创造这个世界的方法论。因此我很是喜欢台湾那边对programming paradigm 的翻译:程式設計法。网络

为何我要强调编程范例是什么东西,并且还分门别类的列举了出来这些编程范例呢?app

由于编程自己是抽象的,编程范例其实就是咱们如何抽象这个世界的方法,我只是想经过这个具体的定义来讲明**函数式自己就是一种方法论。**因此咱们学习的时候不必惧怕它,遇到引用透明,反作用,科里化,函子,单子,惰性求值等等等等这些概念的时候,畏惧的缘由只是不熟悉而已,就想咱们学习面向对象的时候:继承,封装,多态,动态绑定,消息传递等等等等,这些概念咱们一开始也不熟悉,因此当咱们熟悉了函数式这些概念的时候,一切天然水到渠成。 ** 在咱们熟悉的面向对象的编程范式中,咱们知道它的思想是:一切皆对象,而在纯函数式的编程范式中,能够说:一切皆函数。在函数式编程中,函数是一等公民,那什么是一等公民呢?就是它能够做为参数,返回值,也能够赋值给变量,也就是说它的地位实际上是和Int,String, Double等基本类型是同样的,换言之,要像使用基本类型同样去使用它!

不一样的思想就是建立世界的方法论的不一样之处,这里我举个例子,那就是状态,好比登陆的各类状态,维护状态会大大增长系统的复杂性,特别是状态不少的时候,并且引入状态这个概念以后,会带来不少复杂的问题:状态持久化,环境模型等等等,而若是使用面向对象的编程范例,能够将**每个状态都定义为一个对象,**如C#中的状态机的实现,而在函数式编程里呢?**在SICP中提到,状态是随着时间改变的,因此状态是否可使用f(t)来表示呢?**这就是使用函数式的思路来抽象状态。

固然,我这里并非说只能使用一种编程范式,我也并不鼓吹函数式就一直是好的,可是掌握函数式可让咱们在解决问题的时候提供更多的选择,更有效率的解决问题,事实上,咱们解决问题(创造世界)确定会使用不少种方法论即多种编程范式,通常状况下,更现代的编程语言都支持多范式编程,这里用swift里的RxSwift来举例:

public class Observable<Element> : ObservableType {
    internal init()
    
    public func subscribe<Observer>(_ observer: Observer) -> Disposable where Element == Observer.Element, Observer : RxSwift.ObserverType

    public func asObservable() -> Observable<Element>
}

// 观察者
final internal class AnonymousObserver<Element> : ObserverBase<Element> {

    internal typealias EventHandler = (Event<Element>) -> Void

    internal init(_ eventHandler: @escaping EventHandler)

    override internal func onCore(_ event: Event<Element>)
}



extension ObservableType {
    public func flatMap<Source>(_ selector: @escaping (Element) throws -> Source) -> Observable<Source.Element> where Source : RxSwift.ObservableConvertibleType
}

extension ObservableType {
    public func map<Result>(_ transform: @escaping (Element) throws -> Result) -> Observable<Result>
}
复制代码

它的Observable和Observer都抽象成了类,而且添加了相应的行为,承担了相应的职责,这是面向对象范式;它实现了OberveableType协议,而且拓展了该协议,添加了大量的默认实现,这是面向协议范式;它实现了map,和flatMap方法,能够说Observable是一个函数单子(Monad),同时也提供了大量的操做符可供使用和组合,这是函数式范式;同时,总所周知,Reactive框架是一个响应式的框架,因此它也是响应式范式......

更况且,编程能力不就是抽象能力的体现吗?因此我认为掌握函数式是很是必要的!那么具体来讲为何重要呢?

在1984年的时候,John Hughes 有一篇很著名的论文《Why Functional Programming Matters》, 它解答了咱们的疑问。

为何函数式编程重要?

一般网络上的一些文章都会总结它的优势:它没有赋值,没有反作用,没有控制流等等等等,不一样的只是它们对于各个关键词诸如引用透明,无反作用的种种解释,单是这只是列出了不少函数式程序**"没有"什么,却没有说它“有”什么,因此这些优势其实没有太大的说服力。并且咱们实际上去写程序的时候,也不可能特地去写一个缺乏了赋值语句或者特别引用透明**的程序,这也不是衡量质量的尺度,那么真正重要的是什么呢?

在这篇论文中提到,模块化设计是成功的程序化设计的关键,这一观点已经被广泛接受了,但有一点常常容易被忽略,那就是编写一个模块化程序解决问题的时候,程序员首先要把问题分解为子问题,而后解决这些子问题并把解决方案合并。程序员可以以什么方式分解问题,直接取决于他能以什么方式把解决方案粘起来。而函数式范式其实提供给咱们很是重要的粘合剂,它可让咱们设计一些更小、更简洁、更通用的模块,同时使用黏合剂粘合起来。

那么它提供了哪些黏合剂呢?这篇论文介绍了两种:

黏合函数:高阶函数

The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.

黏合简单的函数变为更复杂的函数。这样的好处是咱们模块化的颗粒度是更细的,能够组合的复杂函数也是更多的。若是非要作一个比喻的话,我以为就像乐高的基础组件: 截屏2021-03-18 上午11.13.02.png 这种聚合就是一个泛化的高阶函数和一些特化函数的聚合,这样的高阶函数一旦定义,不少操做均可以很容易地编写出来。

黏合程序:惰性求值

The other new kind of glue that functional languages provide enables whole programs to be glued together.

函数式语言提供的另外一种黏合剂就是可使得程序黏在一块儿。假设有这么一个函数:

g(f(input))
复制代码

传统上,须要先计算f,而后再计算g,这是经过将f的输出存储在临时文件中实现的,这种方法的问题是临时文件会占用太大的空间,会让程序之间的黏合变得不太现实。而函数式语言提供的这一种解决方案,程序f和g严格的同步运行,只有当g视图读取输入时,f才启动。这种求值方式尽量得少运行,所以被称为**"惰性求值"**。它将程序模块化为一个产生大量可能解的生成器与一个选取恰当解的选择器的方案变得可行。

你们若是有时间仍是应该去读读这一篇论文,在论文中,它讲述了三个实例:牛顿-拉夫森求根法,数值微分,数值积分,以及启发性搜索,并使用函数式来实现它们,很是的精彩,这里我就不复述这些实例了。最后我再引用一下该论文的结论:

在本文中,咱们指出模块化是成功的程序设计的关键。以提升生产力为目标的程序语言,必须良好地支持模块化程序设计。可是,新的做用域规则和分块编译的技巧是不够的——“模块化”不只仅意味着“模块”。咱们分解程序的能力直接取决于将解决方案粘在一块儿的能力。为了协助模块化程序设计,程序语言必须提供优良的黏合剂。函数式程序语言提供了两种新的黏合剂——高阶函数惰性求值

一颗枣树(例子)

这个例子我参考了Objc.io的《函数式Swift》书籍中关于如何使用函数式的方式来封装滤镜的案例。

Core Image是一很强大的图像处理框架,可是它的API是弱类型的 —— 能够经过键值编码来配置图像滤镜,这样就致使很容易出错,因此可使用类型来避免这些缘由致使的运行时错误,什么意思呢?就是说咱们能够封装一些基础的滤镜**Filter, **而且还能够实现它们之间的聚合方式。这就是上述论文中介绍的函数式编程提供的黏合剂之一:使简单的函数能够聚合起来造成复杂的函数。

首先肯定咱们的滤镜类型,该函数应该接受一个图像做为参数并返回一个新的图像:

typalias Filter = (CIImage) -> CIImage
复制代码

在这里引用一段书中的原话:

咱们应该谨慎地选择类型。这比其余任何事情都重要,由于类型将左右开发流程。

而后能够开始定义函数来构件特定的基础滤镜了:

/// sobel提取边缘滤镜
func sobel() -> Filter {
    return { image in
        let sobel: [CGFloat] = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
        let weight = CIVector(values: sobel, count: 9)
        guard let filter = CIFilter(name: "CIConvolution3X3",
                                    parameters: [kCIInputWeightsKey: weight,
                                                 kCIInputBiasKey: 0.5,
                                                 kCIInputImageKey: image]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}

/// 颜色反转滤镜
func colorInvert() -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorInvert",
                                    parameters: [kCIInputImageKey: image]) else { fatalError() }
        guard let outImage = filter.outputImage else { fatalError() }
        return outImage.cropped(to: image.extent)
    }
}


/// 颜色变色滤镜
func colorControls(h: NSNumber, s: NSNumber, b: NSNumber) -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputImageKey: image, kCIInputSaturationKey: h, kCIInputContrastKey: s, kCIInputBrightnessKey: b]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}
复制代码

直接黏合

基础组件已经有了,接下来就能够堆积木了。若是有一个滤镜须要:先提取边缘 -> 颜色反转 -> 颜色变色,那么咱们能够实现以下:

let newFilter: Filter = { image in
    return colorControls(h: 97, s: 8, b: 85)(colorInvert()(sobel()(image)))
}
复制代码

上述作法有一些问题:

  • 可读性差:没法代码即注释,没法很容易的知道滤镜的执行顺序
  • 不易拓展:API不友好,添加新的滤镜时,须要考虑顺序和括号,很容易出错

自定义函数黏合

首先咱们解决可读性差的问题,由于直接使用嵌套调用方法,因此会可读性差。因此咱们要避免嵌套调用,直接定义combine方法来组合滤镜:

func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

// sobel -> invertColor
let newFilter1: Filter = compose(sobel(), colorInvert()) // 左结合的
复制代码

这是左结合的,因此可读性是OK的,可是若是有三个滤镜组合呢?四个滤镜组合呢?要定义那么多方法吗? 巧了,还真有人是这么干的: 截屏2021-03-18 下午3.03.41.png 若是你们去看RxSwift的话,就会看见它组合多个Observable的函数: zip , combineLastest ,每个方法簇都提供了支持多个参数的组合方法,但是这就意味着咱们在这个案例也是能够这样作的,可是这显然不是最好的解决方案。

若是使用combine这里三个滤镜组合的方案:

let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))
复制代码

可读性还行,可是仍是在添加新的滤镜的时候容易出错,不那么容易拓展。若是要再组合多个滤镜,那么就须要多个combine函数嵌套调用。

自定义操做符黏合

若是对应到数学领域的话,其实这几个滤镜的组合不就是四则运算中的 +  吗?一层一层效果的叠加,固然,确切地说,从效果上和 + 更类似,可是从特性来讲更符合减法 - 的,都是向左结合,并且都不知足交换律。

因此咱们能够自定义操做符来处理滤镜的结合:

infix operator >>>
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}
复制代码

固然还有一个小问题,就是若是有三个滤镜组合的话,会报错,由于咱们没有指定它组合的方式(左结合,仍是右结合)因此这里咱们让它继承加法的优先级,由于它和加法同样都是左结合的:

infix operator >>>: AdditionPrecedence // 让它继承+操做符的优先级, 左结合
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}
复制代码

那接下来咱们愉快地使用它吧:

let filter = sobel() >>> colorInvert() >>> colorControls(h: 97, s: 8, b: 85)
let outputImage = filter(inputImage)
imageView.image = UIImage(ciImage: outputImage)
复制代码

函数式Swift.001.jpeg

那么这里来总结一下这一波过程,假设需求是存在的:

咱们定义了不少基础滤镜层(Filter),接下来确定须要组合基础滤镜为咱们实际需求须要的滤镜,有的滤镜多是有三个基础滤镜组合的,有的须要五个基础滤镜组合,固然极限状况下,可能还有须要十个滤镜组合的。

因此咱们须要定义不一样滤镜组合的**黏合函数,**咱们一共经历了三个组合方案的变迁:

  1. 直接组合
  2. 定义compose函数
  3. 自定义操做符

固然,诸君也可使用更好的组合方案,若是能够但愿留个言,共同探讨探讨。

还有一颗也是枣树(例子)

接下来这个例子,是一个咱们使用Objective-C编程的时候常常会遇到的问题,需求以下:第二行数据必须等待第一行请求结束以后才能够开始请求。 截屏2021-03-31 上午9.58.51.png

那么开始吧!

首先咱们来看最容易的实现方案:

@objc func syncData() {
        self.statusLabel.text = "正在同步火影忍者数据"
        
        WebAPI.requestNaruto { (firstResult) in
            if case .success(let result) = firstResult {
                self.sectionOne = result.map { $0 as? String ?? "" }
                DispatchQueue.main.async {
                    self.tableView.reloadSections([0], with: .automatic)
                    
                    self.statusLabel.text = "正在同步海贼王数据"
                    WebAPI.requestOnePiece { (secondResult) in
                        if case Result.success(let result) = secondResult {
                            self.sectionTwo = result.map { $0 as? String ?? "" }
                            DispatchQueue.main.async {
                                self.statusLabel.text = "同步海贼王数据成功"
                                self.tableView.reloadSections([1], with: .automatic)
                            }
                        }
                    }
                }
            }
        }
    }
复制代码

熟悉吗?固然熟悉,直接在第一个请求的callback中直接进行第二个请求,可是请注意,这和OC写的有区别吗?咱们这样和写和简单的人肉翻译机有区别吗?咱们写的是Swift这个多范式的编程语言吗?

回到例子,咱们就事论事,我以为这样写会有几个问题:

  1. 数据修改和UI修改耦合在了一块儿
  2. 多重嵌套
  3. 违背了OCP(Open Closed Principle)法则:应该对修改闭合,对拓展开放
  4. 丑!

解决数据和UI耦合

从重要性的角度,我以为应该先解决第4个问题,可是出于节奏,咱们仍是从第一个问题开始解决吧~

@objc func syncDataThere() {
        // 嵌套函数
        func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
            DispatchQueue.main.async {
                self.statusLabel.text = text
                if reload.isReload { self.tableView.reloadSections([reload.section], with: .automatic) }
            }
        }
        
        updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
        
        requestNaruto {
            updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
            self.requestOnePiece {
                updateStatus(text: "同步数据成功", reload: (true, 1))
            }
        }
    }
复制代码

这里我把网络请求和数据处理都封装到了网络请求中,并且使用了swift的特性:嵌套函数,剥离了一部分重复代码,这样整个请求就变得很是清晰明了了,并且数据和UI就隔离开来了,并无耦合在一块儿。

但是嵌套的问题仍是存在,如何解决呢?

解决多重嵌套

还记得我介绍的第一棵枣树吗?我使用了自定义操做符来解决了函数调用的嵌套,这里其实也是同样的思路,可是要更复杂些。

这里我还须要重复引用一下《函数式Swift》中的那句话:

咱们应该谨慎地选择类型。这比其余任何事情都重要,由于类型将左右开发流程。

第一步抽象

这里有两个类型须要抽象,第一是执行单个语句的函数(这里是更新UI),第二个是对应网络请求的函数

infix operator ->> AdditionPrecedence
typealias Action = () -> Void
typealias Request = (@escaping Action) -> Void
复制代码

第二步抽象

那么如何将原来的函数拆解为使用类型表示的函数呢?

func syncDataF() {
    ......
	requestNaruto {
    	updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
        self.requestOnePiece {
        	updateStatus(text: "同步数据成功", reload: (true, 1))
        }
	}
)
复制代码

咱们由上往下,那么抽象的过程应该就是

  • (Request, Action) -> Request

第一个请求 和 回调中的第一个Action,可是第一个请求尚未结束,因此返回的仍是Request

  • (Request, Request) -> Request

处理了第一个Action的第一请求 + 第二个请求, 可是请求仍是没有结束,因此返回的仍是Request

  • (Request, Action) -> Action

第二个请求加上最后须要处理的Action , 完毕!

因此结果以下:

@objc func syncDataFour() {
	func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
     	DispatchQueue.main.async {
        	self.statusLabel.text = text
            if reload.isReload { 
                self.tableView.reloadSections([reload.section], with: .automatic) 
            }
        }
    }
    updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
    // 咱们来拆解一下函数:要把函数抽象出来,这一点很是的重要
    // (Request, Action) -> Request
    // (Request, Request) -> Request
    // (Request, Action) -> Action
    // 经过这样的拆解方式就能够开始定义方法了
    let task: Action =
     	requestNaruto
            ->> { updateStatus(text: "正在同步海贼王数据", reload: (true, 0)) }
            ->> requestOnePiece
            ->> { updateStatus(text: "同步数据成功", reload: (true, 1)) }
    task()
}
复制代码

结果呢?我解决了嵌套的问题,很好,很完美,但是也很天真。

解决OCP问题

即便咱们使用了自定义操做符,也没有解决OCP问题,由于若是咱们要添加请求的话,咱们仍是须要修改原来的方法,依然违背了OCP法则。

那么怎么解决呢?

嗯嗯,具体的,请各位本身去试验吧!

我在文章尾部添加了相应的引用信息,这个例子是基于2016年的国内的Swift大会中翁阳的分享《Swift, 改善既有代码的设计》,若是有时间,但愿你们能够去看看这个分享。

在分享中,他使用了面向协议的思路解决了OCP问题,很抽象,很精彩。

总结

很开心诸位看到了这里,我以为这篇文章的能量密度应该不会浪费大家的时间。

在这边文章中,我首先是追问了函数式编程,以及编程范式的定义,只是想告诉你们:函数式编程之因此复杂只是由于咱们不熟悉,同时它也应该是咱们必须的工具。

而后我介绍了《Why Functional Programming Matters》这篇论文,它说明了为何函数式编程重要,提到函数式范式的两大武器:高阶函数和惰性求值。

最后我使用了两颗枣树来给你们看一看Swift语言结合函数式的思想能够有哪些奇妙的化学反应。

那么这一次Swift的一次函数式之旅就结束了。可是仍是想补充几句,每年的WWDC其实Swift都更新了不少的内容,Swift自己也一直在增长新的特性,一直在稳健的迭代着,若是咱们仍是使用Objective-C的思惟去写Swift的话,其实自己是落后于语言发展的。

最后引用王安石的《游褒禅山记》中的一段话:

而世之奇伟、瑰怪,很是之观,常在于险远,而人之所罕至焉,故非有志者不能至也。

与君共勉!

引用

  1. wikipedia. "Functional programming".(en.wikipedia.org/wiki/Functi…)
  2. wikipedia. "Programming paradigm". (en.wikipedia.org/wiki/Progra…)
  3. John Hughes. "Why Functional Programming Matters".(PDF) (www.cs.rice.edu/~javaplt/41…)
  4. objc. "Functional Swift".(eBook)(objccn.io/products/fu…)
  5. 翁阳. "Swift, 改善既有代码的设计".(Video)(www.youtube.com/watch?v=z4r…)
  6. 包函卿. "Swift函数式实践".(Video)(www.youtube.com/watch?v=lf9…)
  7. ScottWlaschin. "The Functional ToolKit".(Video)(www.bilibili.com/video/BV1ex…)
相关文章
相关标签/搜索