内容简介:以前在iOS上,除了RAC,比较少有函数式编程方面的实践。swift对函数式做了较多的支持,随着swift的普及,在iOS社区,函数式编程被越来越多的开发者所接受。并且因为函数式编程的一些优点,也越来越多的语言开始支持函数式的开发范式。因为最近也在项目中开始实践函数式编程,也能逐渐感受到函数式强大之处。目前也有一点心得,本文就谈下自己对函数式编程的理解。
以前在iOS上,除了RAC,比较少有函数式编程方面的实践。swift对函数式做了较多的支持,随着swift的普及,在iOS社区,函数式编程被越来越多的开发者所接受。并且因为函数式编程的一些优点,也越来越多的语言开始支持函数式的开发范式。
因为最近也在项目中开始实践函数式编程,也能逐渐感受到函数式强大之处。目前也有一点心得,本文就谈下自己对函数式编程的理解。
不过在开始之前,先推荐个小段子: 好久不见
编程范式
在讲函数式编程之前,我们先来看看这两大编程范式:
- 命令式
- 声明式
来粘贴一个关于它们的定义:
命令式编程通过一系列改变程序状态的指令来完成计算。命令式编程模拟电脑运算,是行动导向的,关键在于定义解法,即“怎么做”,因而算法是显性而目标是隐性的。
声明式编程只描述程序应该完成的任务。声明式编程模拟人脑思维,是目标驱动的,关键在于描述问题,即“做什么”,因而目标是显性而算法是隐性的。
这两种范式的关键区别就在于,一个主要表达“怎么做”,一个主要表达“做什么”。通过两段代码,可以直观的感受一下它们的区别:
这两段代码都是做的同一件事,把数组里每个元素都乘2,再打印出来。
可以看出来,我们平时使用的主要都是命令式的写法,包括使用的最多的面向对象编程(OOP),其实还是属于命令式编程的范畴。而这篇文章要讲的函数式编程,则是属于声明式编程。
函数式编程的优势
- 减轻 程序员 思考的负担(防秃),降低出错的可能性
就拿上面举的例子来说,先不说一眼望去,命令式写法的复杂性就明显要高得多,即使是一个如此简单的逻辑,也容易写出错误。
当时写这个例子的时候,为了突出它们的区别,我故意没用for in range这种比较常规的写法,而使用了while,自己维护index。然鹅第一次跑起来就死循环了,再检查,发现是红框里的index加1漏了写。
函数式写法中,因为没有了状态的维护,杜绝了这种错误的发生。
- 代码可读性高
- 更简洁、更少的代码
看回定义,声明式编程描述“做什么”,目标是显性而算法是隐性的。而阅读代码时我们往往只关心代码做了什么,而不关心怎么做,这与声明式编程的思想是不谋而合的,所以函数式编程具有更高的代码可读性。
同时为了实现这种思想,算法的实现往往是内建的,或是隐藏的,这也让函数式的实现具有更少的代码,看起来更简洁。
- 适用于并发环境
- 易于优化
函数式编程中所使用的函数,大多要求为纯函数,这里可以先理解为数学意义上的函数,在相同的参数输入的情况下,一定会有同样的输出。
这一特点决定了纯函数可以并行的调用,而无需任何修改,因为无论并行与否,它都能输出正确的结果。
上图parallelMap函数的实现就是将数组分成多段,然后在多个线程中分别调用map,最后再拼回一整个。通过这一点改动,就简单的对这个程序进行了多线程的性能优化。
- 细粒度的重用(函数级别)
- 易于测试
纯函数除了之前说的,还要求“没有任何可观察的副作用,不依赖外部环境的状态”。
后半还是容易理解的,因为只要依赖了外部环境,就不能保证相同输入得到相同输出了。
前半所说的副作用,是指除了返回值外,函数还通过其他方式对调用环境产生了影响,例如修改全局变量,写文件,print到控制台等等。
纯函数很容易进行单元测试,只需关心输入输出即可。重用也是同理,不必考虑调用的顺序,不用担心影响后续逻辑,放心的复用。
- 易于重构
原因也就是前面说的那些,纯函数之间的依赖关系就是一个树,很容易理清。而对象之间的依赖就复杂得多,做过重构的同学应该都有体会,如果没有把代码看的很熟,是不敢轻易去动的。书里有一句话总结的很到位:
函数式编程指南
概念都了解了,那要怎么应用到实际的项目中呢?只要在编写代码时,往下面这几点靠,自然就能写出函数式风格的代码了:
- 只定义纯函数
- 不要用可变量
- 递归代替迭代
- 尽量少的数据结构
- 闭包、高阶函数的应用
- 尽可能使用内置的数据结构
- 尽可能使用内置的函数式工具
但是并非所有功能都适合用函数式来编写,很多情况下副作用是无法避免的:
函数式语言和逻辑式语言擅长基于数理逻辑的应用,命令式语言擅长基于业务逻辑的、尤其是交互式或事件驱动型的应用。
现阶段遇到这些情况时,还是不要强行应用函数式了,特别是苹果的系统框架还是基于面向对象的。
接下来举个:chestnut::
我们项目中有这样一个需求,在一个列表中,有多种元素(文件、文件夹、笔记等)。还有多种操作,每种操作支持的类型、数量不同。现在选中了一些元素,求能对这些元素进行哪些操作。
例如,选中一个文件、一个文件夹,这时可以进行移动操作(批量、不支持笔记),但不能进行重命名操作(只支持单个)。
这是一个很常见的需求,并且不涉及UI、交互,适合用函数式来改造一下。
最终我们需要这样一个东西,输入选择的元素列表,得到支持的操作集合。我们把它定义成Strategy类型,是一类函数,在函数式编程里,函数就是一等公民。
typealias Strategy = (_ items: [Any]) -> Set<WYOperationType>
光有Strategy还不够,我们要直接实现这样一个函数还是太过复杂,而且也不够灵活,需要把它拆解为更小的单元。
我采用的方法是,先定义一个默认的操作集合,包含了最常规的操作。然后定义一些Modifier来修改默认的操作集合。不同类型的元素支持哪些操作就定义在不同的Modifier里。
static let defaultStrategy: Strategy = { _ in [.delete, .share, .safeBoxMoveIn, .safeBoxMoveOut, .shareDirWithFriend, .rename, .download, .jumpToDir, .openIn, .favour, .move, .inbox, .unInbox, .fileInfo, .ocr, .groupMove, .noteGroup, .genPDF, .genGIF] } typealias Modifier = (_ items: [Any], _ operations: Set<WYOperationType>) -> Set<WYOperationType> class func modify(_ strategy: @escaping Strategy, _ modifier: @escaping Modifier) -> Strategy { return { items in modifier(items, strategy(items)) } }
这里的modify函数,其实就是一个带items参数的compose(组合)操作。
下面我们就来定义一个目录的modifier:
static let dirSupported: Operand = { items in let dirs = items.lazy.compactMap { $0 as? WeiyunDir } return dirs.first.map { if $0.isCollecting() { return [.share, .shareDirWithFriend, .safeBoxMoveIn, .move, .inbox, .unInbox, .delete, .rename] } else { return [.share, .shareDirWithFriend, .safeBoxMoveIn, .move, .inbox, .delete, .rename] } } } static let dirModifier: Modifier = buildModifier(dirSupported, intersection)
dirSupported判断items中是否有目录,有的话就返回目录支持的操作,通过buildModifier和intersection,构造了一个“如果含有目录,就将目前的操作集合与目录支持的操作集合取交集”的一个Modifier。
考虑到Modifier其实也就只有支持、不支持、添加三种情况,分别对应集合的交、差、并集,就通过“返回操作集合”的Operand函数,和三种SetOperation来build一个Modifier,将Modifier的逻辑也细分:
typealias SetOperation = (_ first: Set<WYOperationType>, _ second: Set<WYOperationType>) -> Set<WYOperationType> static let union: SetOperation = { $0.union($1) } static let subtracting: SetOperation = { $0.subtracting($1) } static let intersection: SetOperation = { $0.intersection($1) } typealias Operand = (_ items: [Any]) -> Set<WYOperationType>? class func buildModifier(_ operand: @escaping Operand, _ setOperation: @escaping SetOperation) -> Modifier { return { items, operations in if let ret = operand(items) { return setOperation(operations, ret) } return operations } }
下面定义了几个用于将Modifier进行组合的函数:
static let emptyModifier: Modifier = { $1 } class func concat(_ modifierLeft: @escaping Modifier, _ modifierRight: @escaping Modifier) -> Modifier { return { items, operations in return modifierRight(items, modifierLeft(items, operations)) } } class func concatModifiers(_ modifiers: [Modifier]) -> Modifier { return modifiers.reduce(emptyModifier) { concat($0, $1) } }
有了Modifier的compose函数,就可以定义更复杂的文件和笔记的Modifier了。
最后还有一个count的Modifier,是专门用来在多选的情况下,去除只支持单元素的操作。
static let fileSupported: Operand = { items in let files = items.lazy.filter { !($0 is WeiyunNote) }.compactMap { $0 as? WeiyunFile } return files.first.map { _ in [.download, .share, .shareDirWithFriend, .fileInfo, .safeBoxMoveIn, .move, .jumpToDir, .delete, .favour, .rename, .openIn, .ocr, .groupMove, .genGIF] } } static let fileUnsupported: Operand = { items in let files = items.lazy.filter { !($0 is WeiyunNote) }.compactMap { $0 as? WeiyunFile } let noOcr = files.filter { !$0.isShouldShowOCRAction() }.first let noImgVid = files.filter { !$0.isImageFile() && !$0.isVideoFile() }.first let noLivePhoto = files.filter { !$0.isLivePhoto }.first let secondFile = files.dropFirst().first let result: [WYOperationType] = (noOcr != nil ? [.ocr] : []) + (noImgVid != nil ? [.groupMove, .genGIF] : []) + (noLivePhoto != nil ? [.genGIF] : []) + (secondFile != nil ? [.genGIF] : []) return Set<WYOperationType>(result) } static let fileModifier: Modifier = concatModifiers([buildModifier(fileSupported, intersection), buildModifier(fileUnsupported, subtracting)]) static let noteSupported: Operand = { items in let notes = items.lazy.compactMap { $0 as? WeiyunNote } return notes.first.map { _ in return [.share, .delete, .noteGroup, .favour] } } static let noteUnsupported: Operand = { items in let notes = items.lazy.compactMap { $0 as? WeiyunNote } return notes.dropFirst().first.map { _ in return [.favour] } } static let noteModifier: Modifier = concatModifiers([buildModifier(noteSupported, intersection), buildModifier(noteUnsupported, subtracting)]) static let countUnsupported: Operand = { items in let wyItems = items.lazy.compactMap { $0 as? WeiyunItem } return wyItems.dropFirst().first.map { _ in return [.ocr, .fileInfo, .rename, .jumpToDir, .openIn, .inbox, .unInbox] } } static let countModifier: Modifier = buildModifier(countUnsupported, subtracting)
最后定义一个applyModifiers来应用一组Modifier:
class func applyModifiers(_ strategy: @escaping Strategy, _ modifiers: [Modifier]) -> Strategy { return modifiers.reduce(strategy) { modify($0, $1) } } static let filesTabStrategy: Strategy = applyModifiers(defaultStrategy, [dirModifier, fileModifier, noteModifier, countModifier])
filesTabStrategy就可以拿到我们APP的文件列表去用了,选中一些项目后,根据Strategy返回的操作集合来展示操作菜单。
如果其他场景有不同的需求,可以编写自己的Modifier,再与现有的一顿组合即可。
主要思想就是把函数、闭包作为数据来操作,运用于高阶函数。还要熟悉函数式编程的三板斧(map、filter、reduce)和lazy等函数式工具,遵循开头的那几个要点来编写代码,就算是入了函数式的大门了。
其他
-
纯函数的引用透明
引用透明网上有很多种解释,函数式里,是指函数运行的结果与函数本身,是可以互相替代的。显然纯函数是具有引用透明性的,所以可以延迟执行而不影响结果,这是lazy能正确执行的理论前提。
这里给出一个比较有意思的lazy filter的实现:
-
记忆
可以简单的对纯函数进行缓存,提高性能。
Swift里虽然没有内建的实现,但是自己实现一个也很简单:
生产环境使用时还要考虑内存占用、淘汰等。
-
递归代替迭代
这里实现的是“每隔1、2、3、4……个数,取一个数”。
不能使用var时,我们需要将迭代换成递归实现。好处就不再赘述了,并且符合函数式的原则。
这里需要注意的是要用上尾递归,防止爆栈。
然而我在swift上测试时,还是出现了爆栈的情况,网上查了资料说是swift不保证尾递归优化,好吧……
-
let的性能问题
纯函数式语言大概是不会出现这样的情况的,运行时的优化,数据结构的高度优化,结果应该是跟用var差不多的性能才对。至少以我的知识,都能实现一个插入时间复杂度为O(log n)的不可变字典。
swift估计是没有做这方面的优化,赋值给var时应该是发生了拷贝。并且let的dict也没有插入一个值,返回一个新let dict的方法。
-
运算符重载
-
ReactiveX
将Async、Lazy、Multi-threading版本的函数式工具,引入了面向对象编程,非常值得使用。
总结
虽然说swift引入了许多函数式编程的东西,光从“没有尾递归优化”和“不可变数据结构优化”这两点来看,实际上还是不能完全按照纯函数式语言的那一套来编码的。
函数式编程,现阶段也还不会在项目中大量运用,但是这种思维,确实可以给我们平时的编码带来不一样的启发。
以目前swift的能力来说,一些比较简单的函数式的应用还是可以胜任的。
即使在OO的编程中,这些函数式的 工具 也能够对简化代码、逻辑起到很大的作用。
参考资料
以上所述就是小编给大家介绍的《Swift函数式编程探索》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。