继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Swift的一次函数式之旅

九州编程
关注TA
已关注
手记 475
粉丝 42
获赞 202

本文适合哪些人

本文针对的是已经有一部分Swift开发的基础但是对函数式范式比较感兴趣的开发者。当然如果只对函数式范式感兴趣我觉得这篇文章也值得一看。

函数式编程是什么

首先来看这个词语”Functional Programming“它是什么

当需要去查一个专业术语的定义的时候我的第一反应是来查询Wikipedia

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

在这个定义里有一个很熟悉的词——programming paradigm, 一般翻译为编程范式可是我对这个翻译还是有些迷糊于是我又在wikipedia中查找这个词语的含义:

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

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

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

那么究竟什么是编程范式呢我们知道编程是一门工程学它的目的是去解决问题而解决问题可以有很多的方法编程范例就是代表着解决问题的不同思路。如果说我们是编程世界的造物主的话那么编程范例应该就是我们创造这个世界的方法论。所以我非常喜欢台湾那边对programming paradigm 的翻译程式設計法。

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

因为编程本身是抽象的编程范例其实就是我们如何抽象这个世界的方法我只是想通过这个具体的定义来说明**函数式本身就是一种方法论。**所以我们学习的时候没必要害怕它遇到引用透明副作用科里化函子单子惰性求值等等等等这些概念的时候畏惧的原因只是不熟悉而已就想我们学习面向对象的时候继承封装多态动态绑定消息传递等等等等这些概念我们一开始也不熟悉所以当我们熟悉了函数式这些概念的时候一切自然水到渠成。 ** 在我们熟悉的面向对象的编程范式中我们知道它的思想是一切皆对象而在纯函数式的编程范式中可以说一切皆函数。在函数式编程中函数是一等公民那什么是一等公民呢就是它可以作为参数返回值也可以赋值给变量也就是说它的地位其实是和IntString, 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)法则应该对修改闭合对拓展开放

解决数据和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…)

作者Ryan__
链接https://juejin.cn/post/6953498817224245285
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP