前言
初步深入函数式编程是在寒假的时候,搞了一本Haskell的书,啃了没多久就因为我突然的项目任务被搁置了,不过在学习的时候也是各种看不懂,里面的概念略微抽象,再加上当时没有适当地实战敲Demo,导致没过多久脑袋就全空了。庆幸的是,Swift是一门高度兼容函数式编程范式的语言,而我又是一只喜欢敲Swift的程序Dog,在后来我使用Swift编码时,有意识或无意识地套用函数式编程范式的一些概念,也渐渐加深我对函数式编程的理解。这篇文章是我对自己所掌握的函数式编程的一个小总结,主要探讨的是函数式编程中的几个概念: Functor
、Applicative
、Monad
以及它们在Swift中的表现形式。由于本人能力有限,一些概念上的不严谨、编码上的不全面希望大家多包涵,欢迎留下各位宝贵的意见或问题。
本文为纯概念讲述,后期或许会有函数式编程实战的文章推出(我有空写再说吧)
概念
Context
在编码时,我们会遇到各种数据类型,基础的数据类型我们称作值
,当然这并不是指编程语言中的基本数据类型,比如说整形1
它可以称作一个值,一个结构体struct Person { let name: String; let age: Int }
的实例也可以成为一个值,那么何为Context(上下文)
呢,我们可以将它理解为对值的一个包装,通过这层包装,我们可以得知值此时所处在的一个状态。在Haskell中,这个包装就是typeclass(类型类)
,而在Swift中,魔性的enum(枚举)
可以充当这个角色,一个例子,就是Swift中的Optional(可选类型)
,它的定义如下(相关继承或协议关系在这里不标出):
Optional {
case none
case some(Wrapped)
Optional有两种状态,一种是空状态none
,也就是和平时我们传入的nil
相等价,一种是存在值的状态,泛型Wrapped
指代被包入这层上下文的值的类型。通过这个例子,我们可以很直观地理解Context
:描述值在某一阶段的状态。当然,在平时开发中,我们会见到各种Context
,比如Either:
enum Either {
case left(L)
case right(R)
}
它代表在某个阶段值可能在left
或者right
中存在。
在一些函数式响应式编程框架如ReactiveCocoa
、RxSwift
中,Context
无处不在:RACSignal
、Observable
,甚至是Swift的基本类型Array(数组)
它本身也可以看作是一个Context
。可见,只要你接触了函数式编程,Context
即会接触。
这里,我特别说下这个Context:Result
,因为在后面对其他概念以及实战的讲述中我都会以它为基础:
enum Result {
case success(T)
case failure(MyError)
}
Result
上下文存在两种状态,一种是成功的状态,当处于这个状态,Result
就会持有一个特定类型的值,另外一种状态是失败状态,在这个状态中,你可以获取到一个错误的实例(这个实例可以是你自己拟定的)。这么这个Context有什么用呢?想象一下,你正在进行一项网络操作,获取到的数据是无法确定的,你或许能如你所愿,从服务器中获取到你期望的值,但是也有可能此时服务器发生一些未知的错误,或者网络延时,又或是一些不可抗力的影响,那么,此时你得到的将会是一个错误的表示,如HTTP Code 500...而Result
可以在这种情况下引入来表示你在网络操作中获取到的最终结果,是成功还是失败。除了网络请求,诸如数据库操作、数据解析等等,Result
都可以引入来进行更明确的标示。
何为Functor、Applicative、Monad?
你可以把Functor
、Applicative
、Monad
想象成Swift中的Protocol(协议)
,它们可以为某种数据结构的抽象,而这种数据接口正是刚刚我在上面提到的Context
,要将某个Context实现成Functor
、Applicative
、Monad
,你必须实现其中特定的函数,所以,要了解什么是Functor
、Applicative
、Monad
,你需要知道它们定义了那些协议函数。接下来我会一一讲解。
Functor
我们对一个值的运算操作使用的是函数,比如我要对一个整形的值进行翻倍操作,我们可以定义一个函数:
func double(_ value: Int) -> Int {
return 2 * value
}
然后就可以拿这个函数对特定的值进行操作:
let a = 2
let b = double(a)
好,问题来了,如果此时这个值被包在一个Context中呢?
一个函数只能作用于它声明好的特定类型的值,运算整形的函数不能用来运算一个非整形的Context,所以这时,我们引入了Functor
。它要做的,就是使一个只能运算值的函数用来运算一个包有这个值类型的Context,最后返回的一个包有运算结果的Context,为此,我们要实现map
这个函数(在Haskell中为fmap),它的伪代码是这样的:
Context(结果值) = map(Context(初始值), 运算函数)
现在我们拿Result
来实现一下:
extension Result {
func map(_ mapper: (T) -> O) -> Result {
switch self {
case .failure(let error):
return .failure(error)
case .success(let value):
return .success(mapper(value))
}
}
}
我们可以看到,首先我们对Result
进行模式匹配,当此时状态是失败的话,我们也直接返回失败,并把错误的实例传递下去,如果状态是成功的,我们就对初始的值进行运算,最后返回包有结果值的成功状态。
为了后面表达式简便,我在这里定义了map
的运算符<^>
:
precedencegroup ChainingPrecedence {
associativity: left
higherThan: TernaryPrecedence
}
// Functor
infix operator <^> : ChainingPrecedence
// For Result
func <^>(lhs: (T) -> O, rhs: Result) -> Result {
return rhs.map(lhs)
}
我们现在就可以测试一下:
let a: Result = .success(2)
let b = double <^> a
在上面我提到,Swift的数组也可以当成是Context,它是作为一个包有多个值的状态存在。想必在日常开发中我们经常也用到了Swift数组中的map
函数吧:
let arrA = [1, 2, 3, 4, 5]
let arrB = arrA.map(double)
RxSwift
中我们也经常使用map
:
let ob = Observable.just(1).map(double)
Applicative
Applicative
其实就是高级的Functor
,我们可以调出上面Functor
的map
伪代码:
Context(结果值) = map(Context(初始值), 运算函数)
在函数式编程中,函数也可以作为一个值来看待,若此时这个函数也是被一个Context包裹的,单纯的map
是不能接受包裹着函数的Context,所以我们引入了Applicative
:
Context(结果值) = apply(Context(初始值), Context(运算函数))
我们将Result
实现Applicative
:
extension Result {
func apply(_ mapper: Result<(T) -> O>) -> Result {
switch mapper {
case .failure(let error):
return .failure(error)
case .success(let function):
return self.map(function)
}
}
}
// Applicative
infix operator <*> : ChainingPrecedence
// For Result
func <*>(lhs: Result<(T) -> O>, rhs: Result) -> Result {
return rhs.apply(lhs)
}
使用:
let function: Result<(Int) -> Int> = .success(double)
let a: Result = .success(2)
let b = function <*> a
Applicative
在日常开发中其实用的不多,很多时候我们并不会将一个函数塞进一个Context上,但是如果你用了一些略为高阶的函数时,它强劲的能力就能在此时表现出来,这里举一个略为晦涩的例子,你可以花点时间搞懂它:
这个例子的思路是来自源Swift的函数式JSON解析库Argo
的基本用法,若大家有兴趣可以阅读下Argo
的源码: thoughtbot/Argo
假设现在我定义了一个函数,它能够接受一个Any
的JSON Object,以及一个值在JSON中对应的Key(键)
作为参数,返回一个从JSON数据中解析出来的结果,由于这个结果是不确定的,可能JSON中不存在此键对应的值,所以我们用Result
来包装它,这个函数的签名为:
func parse(jsonObject: Any, key: String) -> Result
当解析成功时,返回的Result
处于成功状态,当解析失败时,返回的Result
处于失败状态并携带错误的实体,我们能够通过错误实体得知解析失败的原因。
现在我们有一个结构体,它里面有多个成员,它实现了默认的构造器:
struct Person {
let name: String
let age: Int
let from: String
}
我们自己可以编写一套函数柯里化的库,这个库能够对多参数的函数进行柯里化,你也可以从Github中下载: thoughtbot/Curry
比如,我们有一个函数,它的基本签名是: func haha(a: Int, b: Int, c: Int) -> Int
,通过函数柯里化我们可以将其转化为(Int) -> (Int) -> (Int) -> Int
类型的函数。
我们此时将Person的构造器进行函数柯里化:curry(Person.init)
,此时我们得到的是类型为(String) -> (Int) -> (String) -> Person
的值。
现在奇幻的魔法来了,我定义一个将JSON解析成Person的函数:
func parseJSONToPerson(json: Any) -> Result {
return curry(Person.init)
<^> parse(jsonObject: json, key: "name")
<*> parse(jsonObject: json, key: "age")
<*> parse(jsonObject: json, key: "from")
}
通过这个函数,我能够将一个JSON数据解析成Person的实例,以一个Result
的包装返回,如果解析失败,Result
处理失败状态会携带一个错误的实例。
这个函数为什么可以这么写呢,我们来分解一下:
首先通过函数的柯里化我们得到了类型为(String) -> (Int) -> (String) -> Person
的值,它也是一个函数,然后经过了<^>
map的操作,map的右边是一个解析了name
返回的Result
,它的类型为Result
,map将函数(String) -> (Int) -> (String) -> Person
应用于Result
,此时我们得到的是返回的结果(Int) -> (String) -> Person
的Result包装:Result<(Int) -> (String) -> Person>
(因为已经消费掉了一个参数),此时,这个函数就被一个Context包裹住了,后面我们不能再用map去将这个函数应用在接下来解析出来的数据了,所以这是我们就借助于Applicative
的<*>
,接下来看第二个参数,parse
函数将JSON解析返回了类型为Result
的结果,我们通过<*>
将Result<(Int) -> (String) -> Person>
的函数取出来,应用于Result
,就得到了类型为Result<(String) -> Person>
的结果。以此类推,最终我们就获取到了经JSON解析后的结果Result
。
Applicative
强大的能力能够让代码变得如此优雅,这就是函数式编程的魅力之所在。
Monad
Monad
中文称为单子
,网上看到挺多人被Monad
的概念所搞晕,其实它也是基于上面所讲述的概念而来的。对于使用过函数式响应式编程框架(Rx
系列[RxSwift、RxJava]、ReactiveCocoa)的人来说,可能不知道Monad
是什么,但是在实战中肯定用过,它所要求实现的函数说白了就是flatMap
:
let ob = Observable.just(1).flatMap { num in
Observable.just("The number is \(num)")
}
有很多人喜欢用降维
来形容flatMap
的能力,但是,它能做的,不止如此。
Monad
需要实现的函数我们可以称为bind
,在Haskell
中它使用符号>>=
,在Swift
中我们可以定义运算符>>-
来表示bind函数,或者直接叫做flatMap
。我们先来看看他的伪代码:
首先我们定义一个函数,他的作用是将一个值进行包装,这里标示出这个函数的签名:
function :: 值A -> Context(值B)
(值A与值B的类型可相同亦可不同)
我们的bind
函数就可以这么写了:
Context(结果值) = Context(初始值) >>- function
这里我们实现一下Result
的Monad
:
extension Result {
func flatMap(_ mapper: (T) -> Result) -> Result {
switch self {
case .failure(let error):
return .failure(error)
case .success(let value):
return mapper(value)
}
}
}
// Monad
infix operator >>- : ChainingPrecedence
// For Result
func >>-(lhs: Result, rhs: (T) -> Result) -> Result {
return lhs.flatMap(rhs)
}
Monad
的定义很简单,但是Monad
究竟能帮我们解决什么问题呢?它要怎么使用呢?别急,通过以下这个例子,你就能对Monad
有更深一层的理解:
假设现在我有一系列的操作:
- 通过特定条件进行本地数据库的查询,找出相关的数据
- 利用上面从数据库得到的数据作为参数,向服务器发起请求,获取响应数据
- 将从网络获取到的原始数据转换成JSON数据
- 将JSON数据进行解析,返回最终解析完成的有特定类型的实体
对以上操作的分析,我们能得知以上每一个操作它的最终结果都具有不确定性,意思就是说我们无法保证操作百分百完成,能成功返回我们想要的数据,所以我们很容易就会想到利用上面已经定义的Context:Reuslt
将获取到的结果进行包裹,若获取结果成功,Result
将携带结果值处于成功状态,若获取结果失败,Result
将携带错误的信息处于失败状态。
现在,我们针对以上每种操作进行函数定义:
// A代表从数据库查找数据的条件的类型
// B代表期望数据库返回结果的类型
func fetchFromDatabase(conditions: A) -> Result { ... }
// B类型作为网络请求的参数类型发起网络请求
// 获取到的数据为C类型,可能是原始字符串或者是二进制
func requestNetwork(parameters: B) -> Result { ... }
// 将获取到的原始数据类型转换成JSON数据
func dataToJSON(data: C) -> Result { ... }
// 将JSON进行解析输出实体
func parse(json: JSON) -> Result { ... }
现在我们假设所有的操作都是在同一条线程中进行的(非UI线程),如果我们只是纯粹地用基本的方法去调用这些函数,我们可能要这么来:
var entityResult: Entity?
if case .success(let b) = fetchFromDatabase(conditions: XXX) {
if case .success(let c) = requestNetwork(parameters: b) {
if case .success(let json) = dataToJSON(data: c) {
if case .success(let entity) = parse(json: json) {
entityResult = entity
}
}
}
}
这代码写起来也好看起来也好真的是一把辛酸泪啊,而且,这里还有一个缺陷,就是我们无法从中获取到错误的信息,如果我们还想要获取到错误的信息,必须再编写多一大串代码了。
此时,Monad
出场了:
let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork >>- dataToJSON >>- parse
吓到了吧,只需一行代码,即可将所有要做的事情连串起来了,并且,最终我们获取到的是经Result
包装的数据,若在操作的过程中发生错误,错误的信息也记录在里面了。
这就是Monad
的威力
当然,我们可以继续对上面的操作进行优化,比如说现在我需要在网络请求的函数中加多一个参数,表示请求的URL,我们可以这样来定义这个网络请求函数:
// B类型作为网络请求的参数类型发起网络请求
// 获取到的数据为C类型,可能是原始字符串或者是二进制
func requestNetwork(urlString: String) -> (B) -> Result {
return { parameters in
return { ... }
}
}
调用的时候我们只需要这样调用:
let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork(urlString: "XXX.com/XXX/XXX") >>- dataToJSON >>- parse
这主要是高阶函数的使用技巧。
个人对Monad
作用的总结有两部分:
- 对一系列针对值与Context的操作进行链式结合,代码极其优雅,清晰明了。
- 将值与Context之间的转换、Context内部进行的操作对外屏蔽,像上面我用原始的方式进行操作,我们需要手动地分析Context的情况,手动地针对不同的Context状态进行相应的操作,而如果我们使用
Monad
,整一流程下来我们什么都不需要做,坐享其成,取得最终的结果。
总结
Swift是一门高度适配函数式编程范式的语言,你可以在里面到处都能找到函数式编程思想的身影,通过上面对Functor
、Appliactive
、Monad
相关概念的讲述,在巩固我对函数式编程的知识外,希望也能让你对函数式编程的理解有帮助,若文章有概念不严谨的地方或者错误,望见谅,也希望能够向我提出。
谢谢阅读。
参考链接
阮一峰的网络日志 - 图解 Monad