本文针对的是已经有一部分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
的翻译:程式設計法。
为什么我要强调编程范例是什么东西,而且还分门别类的列举了出来这些编程范例呢?
因为编程本身是抽象的,编程范例其实就是我们如何抽象这个世界的方法,我只是想通过这个具体的定义来说明函数式本身就是一种方法论。所以我们学习的时候没必要害怕它,遇到引用透明,副作用,科里化,函子,单子,惰性求值等等等等这些概念的时候,畏惧的原因只是不熟悉而已,就想我们学习面向对象的时候:继承,封装,多态,动态绑定,消息传递等等等等,这些概念我们一开始也不熟悉,所以当我们熟悉了函数式这些概念的时候,一切自然水到渠成。 在我们熟悉的面向对象的编程范式中,我们知道它的思想是:一切皆对象,而在纯函数式的编程范式中,可以说:一切皆函数。在函数式编程中,函数是一等公民,那什么是一等公民呢?就是它可以作为参数,返回值,也可以赋值给变量,也就是说它的地位其实是和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.
黏合简单的函数变为更复杂的函数。这样的好处是我们模块化的颗粒度是更细的,可以组合的复杂函数也是更多的。如果非要做一个比喻的话,我觉得就像乐高的基础组件:
这种聚合就是一个泛化的高阶函数和一些特化函数的聚合,这样的高阶函数一旦定义,很多操作都可以很容易地编写出来。
❝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)))
}
上述做法有一些问题:
首先我们解决可读性差的问题,因为直接使用嵌套调用方法,所以会可读性差。所以我们要避免嵌套调用,直接定义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的,但是如果有三个滤镜组合呢?四个滤镜组合呢?要定义那么多方法吗? 巧了,还真有人是这么干的:
如果大家去看RxSwift的话,就会看见它组合多个Observable的函数: <span style="letter-spacing: normal;">zip
, <span style="letter-spacing: normal;">combineLastest
,每一个方法簇都提供了支持多个参数的组合方法,可是这就意味着我们在这个案例也是可以这样做的,但是这显然不是最好的解决方案。
如果使用combine这里三个滤镜组合的方案:
let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))
可读性还行,但是还是在添加新的滤镜的时候容易出错,不那么容易拓展。如果要再组合多个滤镜,那么就需要多个combine函数嵌套调用。
如果对应到数学领域的话,其实这几个滤镜的组合不就是四则运算中的 <span style="letter-spacing: normal;">+
吗?一层一层效果的叠加,当然,确切地说,从效果上和 <span style="letter-spacing: normal;">+
更相似,但是从特性来说更符合减法 <span style="letter-spacing: normal;">-
的,都是向左结合,而且都不满足交换律。
所以我们可以自定义操作符来处理滤镜的结合:
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),接下来肯定需要组合基础滤镜为我们实际需求需要的滤镜,有的滤镜可能是有三个基础滤镜组合的,有的需要五个基础滤镜组合,当然极限情况下,可能还有需要十个滤镜组合的。
所以我们需要定义不同滤镜组合的黏合函数,我们一共经历了三个组合方案的变迁:
当然,诸君也可以使用更好的组合方案,如果可以希望留个言,共同探讨探讨。
接下来这个例子,是一个我们使用Objective-C编程的时候经常会遇到的问题,需求如下:第二行数据必须等待第一行请求结束之后才可以开始请求。
那么开始吧!
首先我们来看最容易的实现方案:
@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这个多范式的编程语言吗?
回到例子,我们就事论事,我觉得这样写会有几个问题:
从重要性的角度,我觉得应该先解决第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法则。
那么怎么解决呢?
嗯嗯,具体的,请各位自己去试验吧!
我在文章尾部添加了相应的引用信息,这个例子是基于2016年的国内的Swift大会中翁阳的分享《Swift, 改善既有代码的设计》,如果有时间,希望大家可以去看看这个分享。
在分享中,他使用了面向协议的思路解决了OCP问题,很抽象,很精彩。
很开心诸位看到了这里,我觉得这篇文章的能量密度应该不会浪费你们的时间。
在这边文章中,我首先是追问了函数式编程,以及编程范式的定义,只是想告诉大家:函数式编程之所以复杂只是因为我们不熟悉,同时它也应该是我们必须的工具。
然后我介绍了《Why Functional Programming Matters》这篇论文,它说明了为什么函数式编程重要,提到函数式范式的两大武器:高阶函数和惰性求值。
最后我使用了两颗枣树来给大家看一看Swift语言结合函数式的思想可以有哪些奇妙的化学反应。
那么这一次Swift的一次函数式之旅就结束了。但是还是想补充几句,每一年的WWDC其实Swift都更新了很多的内容,Swift本身也一直在增加新的特性,一直在稳健的迭代着,如果我们还是使用Objective-C的思维去写Swift的话,其实本身是落后于语言发展的。
最后引用王安石的《游褒禅山记》中的一段话:
❝而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。
与君共勉!
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/yiF0NwXffrkunGOieWbIRA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。