Swift
语言是支持函数式编程的,所以我们需要简单了解一下函数式编程的概念.
在了解函数式编程的概念之前呢,先看看Swfit
中Array
常用的几个方法,因为这几个方法在设计上都是按照函数式编程的规范去设计的.
Array
的常见用法:
1.array.map
映射
public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
map
方法会传入一个闭包,并且返回一个数组.这个闭包会接收一个参数,并且返回一个泛型.参数就是遍历数组中的每一个元素.返回值就是将数组元素映射成一个新值.
var arr1 = [1,2,3,4]
var arr2 = arr1.map {
i in // i 就是数组元素1,2,3,4
i * 2 //对数组元素 x2 ,并且放到一个新数组中
}
//简写
var arr3 = arr1.map { $0 * 2 }
map
传入的是一个闭包,也就是一个函数,所以我们可以直接传入一个函数:
func double(_ num: Int) -> Int{
num * 2
}
var arr4 = arr1.map(double)
2.array.filter
过滤
func filter(_ isIncluded: (T) throws -> Bool) rethrows -> [T]
filter
传入一个闭包参数,返回一个新数组.这个闭包会接收一个参数,此参数是遍历数组中的每一个元素.返回值是一个Bool
值,为true
表示将元素放入新数组,false
表示不放入新数组.
var arr1 = [1,2,3,4]
var arr2 = arr1.filter { (i) -> Bool in
i % 2 == 0
}
//简写
var arr3 = arr1.filter{ $0 % 2 == 0}
3. array.reduce
public func reduce<T>(_ initialResult: T, _ nextPartialResult: (T, Output) -> T) -> Result<T, Just<Output>.Failure>.Publisher
reduce
会遍历数组中的每一个元素,我们拿到数组元素后可以进行相应的操作,并且操作的结果会带到下一次遍历中.
参数解析:
initialResult : T
: 初始化参数,我们可以传入一个任意类型的值,但要和数组元素类型相匹配.
_ nextPartialResult: (T, Output) -> T)
: 闭包.这个闭包会接收两个参数,第一个参数是上一次遍历的结果,如果是第一次遍历则是初始化参数;第二个参数是遍历数组的每一个元素.
var arr1 = [1,2,3,4]
var num = arr1.reduce(0) { (n, i) -> Int in
print("- ", i)
return n + i
}
//打印结果是 0 + 1 + 2 + 3 + 4 = 10
print(num)
//简写
var num2 = arr1.reduce(0){$0 + $1}
map
和flatMap
map
的功能就是把数组元素通过一定规则映射为另一个元素.
那么flatMap
呢?
我们看看下面代码:
var arr = [1,2,3,4]
var arr1 = arr.map {
i in
Array.init(repeating: i, count: i)
}
print("arr1 - " , arr1)
var arr2 = arr.flatMap {
i in
Array.init(repeating: i, count: i)
}
print("arr22 - " , arr2)
运行结果如下:
可以看到通过map
映射后的数组里面存放了4个数组;而通过flatMap
映射的后的数组还是一个数组.所以他们区别就很明显了.
我们看看flatMap
的方法声明:
public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence
可以看到
flatMap返回值里面放的是闭包返回值的
Element,也就是取出元素.而
map的返回值是不管闭包返回值是什么,直接放到数组中.
map
和compactMap
compactMap
:遍历数组的每个元素,返回一个不包含nil
,不包含可选项的新数组(也就是说如果是可选值,会解包后放入新数组
).
var arr = ["1","a","3","b"]
var arr1 = arr.map {
i in
Int(i)
}
print("arr1 -: ",arr1)
var arr2 = arr.compactMap {
i in
Int(i)
}
print("arr2 -: ",arr2)
打印结果:
使用reduce
实现map
, filter
功能:
我们还可以使用reduce
实现和map
,filter
同样的功能.
reduce
实现map
:
var arr = [1,2,3,4]
var arr2 = arr.map { i in
i * 2
}
var arr3 = arr.reduce([]) { (n, i) -> [Int] in
n + [i * 2]
}
print("arr2 - ", arr2)
print("arr3 - ", arr3)
reduce
实现filter
:
var arr = [1,2,3,4]
var arr2 = arr.filter {
i in
i % 2 == 0
}
var arr3 = arr.reduce([]) { (n, i) -> [Int] in
i % 2 == 0 ? n + [i] : n
}
print("arr2 -: ",arr2)
print("arr3 -: ",arr3)
lazy
的优化:
我们看下面代码:
var arr = [1,2,3]
var arr2 = arr.map {
i -> Int in
print("mapping \(i)")
return i * 2
}
print(arr2)
一运行程序,arr
的所有元素都会被映射成新的元素[2,4,6]
,即使我们没有用到数组arr2
.这样肯定是不合理的,如果说arr
中有很多元素,并且映射过程也比较复杂,那么就会造成额外的开销.
所以swift
添加了一个lazy
方法,等用到某个元素的时候才会去映射:
var arr = [1,2,3]
var arr2 = arr.lazy.map {
i -> Int in
print("mapping \(i)")
return i * 2
}
print("--- start map ----")
print("mapped \(arr2[0])")
print("mapped \(arr2[1])")
print("mapped \(arr2[2])")
print("--- end map ----")
optional
的map
var num1: Int? = 10
var num2 = num1.map { $0 * 2 }
print(num2)
var num3: Int? = nil
var num4 = num3.map { $0 * 2 }
print(num4)
上面的代码,可选类型在进行map
时,map
会先判断可选类型是否为nil
,如果为nil
就直接返回nil
,根本不会调用闭包;如果不为nil
,才会调用闭包,并且映射的结果包装成可选项返回.
所以下面两行代码是等价的:
var num1: Int? = 10
var num2 = num1.map { $0 * 2 }
var num3 = num1 != nil ? (num1! * 2) : nil
所以,只要涉及到判断可选项是否为nil
的操作都可以使用map
:
示例一: 字符串拼接变量
var num: Int? = 10
var text1 = num != nil ? "num is \(num!)" : "no num"
print(text1) // num is 10
var text2 = num.map{"num is \($0)"} ?? "no num"
print(text2) // num is 10
示例二: 通过姓名从数组中找到某个人
struct Person{
var name: String
var age: Int
}
var persons = [
Person(name: "张三", age: 18),
Person(name: "李四", age: 18),
Person(name: "王五", age: 18),
]
//原始方法
func findPersonWithName1(_ name: String?) -> Person?{
let index = persons.firstIndex { $0.name == name }
return index != nil ? persons[index!] : nil
}
var p1 = findPersonWithName1("王五")
print(p1!)
//使用 map
func findPersonWithName2(_ name: String?) -> Person?{
persons.firstIndex { $0.name == name }.map { persons[$0] }
}
var p2 = findPersonWithName2("王五")
print(p2!)
Optional
的map 和 flatMap
的区别:
var num1: Int? = 10
var num2 = num1.map { Optional.some($0 * 2)}
print(num2) //Optional(Optional(20))
var num3 = num1.flatMap { Optional.some($0 * 2)}
print(num3) //Optional(20)
flatMap
和map
的功能是一样的,只不过flatMap
如果发现映射的结果本身就是可选项类型,那么它就不会再封装一层可选项;而map
不管结果是什么都会再封装成可选类型.
知道了map
和flatMap
的区别.我们看看flatMap
具体有什么作用:
示例一: 字符串转Date
var dateFmt = DateFormatter()
dateFmt.dateFormat = "yyyy-MM-dd"
var dateStr: String? = "2020-07-02"
var date1 = dateStr != nil ? dateFmt.date(from: dateStr!) : nil
print(date1!)
如果要把一个字符串转成Date
日期类型,按照之前做法就像上面那样.
使用flatMap
也能实现,并且更简洁:
var date2 = dateStr.flatMap {dateFmt.date(from: $0)
}
//由于 dateStr.flatMap 要求传入一个 String -> T? 的闭包
//刚好 dateFmt.date() 就是传入一个 String 返回一个 Date
//完全符合,所以我们可以直接传入 dateFmt.date 函数进去
var date3 = dateStr.flatMap(dateFmt.date)
为什么这里要用flatMap
而不是map
呢?
因为 dateFmt.date() 返回的是一个可选项类型 Date? , 所以 flatMap 不会对返回结果再进行一次可选项包装
示例二: 字典转模型
struct Person{
var name: String
var age: Int
}
func dic2Model(_ dic: [String: Any]) -> Person?{
guard let name = dic["name"] as? String,
let age = dic["age"] as? Int else {
return nil
}
return Person(name: name, age: age)
}
var json: Dictionary? = ["name": "张三", "age" : 18]
var p1 = json != nil ? dic2Model(json!) : nil
//dic2Model返回的是可选项类型,flatMap 不会再封装一层可选项
var p2 = json.flatMap { dic2Model($0) }
print(p1)
print(p2)
函数式编程:
比如说现在有这样一个需求,用函数实现这样的运算[(3 + 8) * 7 / 2] - 1
大家首先想到的肯定会这样做:
func add(_ v1: Int, _ v2: Int) -> Int{
v1 + v2
}
func sub(_ v1: Int, _ v2: Int) -> Int{
v1 - v2
}
func multiple(_ v1: Int, _ v2: Int) -> Int{
v1 * v2
}
func divide(_ v1: Int, _ v2: Int) -> Int{
v1 / v2
}
print(sub(divide(multiple(add(3, 8), 7), 2), 1))
定义4个方法,分别是+ - * /
,这样的确可以满足需求,但是在调用方法时代码的可读性很差,让人看不明白.如果用函数式编程,就会使代码更直观更易懂.
我们对上面代码进行两步改造,实现函数式编程.
第一步: 把上面接受两个参数的函数,升级为只接受一个参数,返回一个函数:
比如对add
函数的升级:
func add(_ v1: Int, _ v2: Int) -> Int{
v1 + v2
}
升级为:
func add(_ v1: Int) -> (Int) -> Int{
{
(v2: Int) -> Int in
v2 + v1
}
}
现在升级后的函数同样可以实现+
运算:
print(add(2)(3)) //5
如上图所示,2是函数add
的参数,3是闭包的参数,在闭包体内将他们相加,并返回一个闭包.
所以可以简化如下:
func add(_ v1: Int) -> (Int) -> Int{{ $0 + v1 }}
依次对其他方法简化:
func add(_ v1: Int) -> (Int) -> Int{{ $0 + v1 }}
func sub(_ v1: Int) -> (Int) -> Int{{ $0 - v1 }}
func multiple(_ v1: Int) -> (Int) -> Int{{ $0 * v1 }}
func divide(_ v1: Int) -> (Int) -> Int{{ $0 / v1 }}
// [(3 + 8) * 7 / 2] - 1
print(sub(1)(divide(2)(multiple(7)(add(8)(3)))))
可以看到简化后的函数可读性还是很差.所以我们还要进行升级.
第二步: 函数合成.把两个函数组合成一个函数.上一个函数的返回值作为下一个函数的参数:
func compose(_ f1: @escaping (Int) -> Int, _ f2: @escaping (Int) -> Int) -> (Int) -> Int{
{
(v1: Int) -> Int in
f2(f1(v1))
}
}
var fn = compose(add(8), multiple(7))
print(fn(3))
上面的compose
函数就实现了把add , multiple
两个函数组合成一个函数并返回.实现了( 3 + 8) * 7
的效果.
可能有人到这里有点懵,它到底是怎么做到的呢?
我们好好梳理一下:
首先compose
会返回一个函数fn
,我们调用fn(3)
,就是把这个3
作为参数传递给了v1
,compose
的两个参数分别是add(8) , multiple(7)
.在compose
返回值的闭包里会先进行f1(v1)
运算,也就是3 + 8
.然后把结果作为参数传递给了f2
,也就是mutiple(7)(11)
.所以最后的结果是77
.
所以现在compose
函数能进行( 3 + 8 ) * 5
运算,也就是能连接两个运算符.那我们完全可以把compose
函数定义为运算符,比如这样:
infix operator >>> : AdditionPrecedence
func >>>(_ f1: @escaping (Int) -> Int, _ f2: @escaping (Int) -> Int) -> (Int) -> Int{
{
(v1: Int) -> Int in
f2(f1(v1))
}
}
这样就定义了一个像 +
一样的运算符,我们可以把一连串的运算组合起来:
// [(3 + 8) * 7 / 2] - 1
var fn = add(8) >>> multiple(7) >>> divide(2) >>> sub(1)
print(fn(3))
并且非常的容易看懂,符合我们的运算习惯.先 + 8,然后 x7,然后 ➗2 ,最后 - 1
.
但是这样还不够通用,因为现在只适用于Int
类型,所以我们要使用泛型,让这个运算符更加通用:
如上图所示,红色部分是运算符>>>
的入口,它和f1
的参数是同一类型;
绿色部分是f1
的返回值,同时也是f2
的参数,所以它俩是同一类型;
蓝色部分是f2
的返回值,同时也是运算符的返回值,所以它俩是同一类型.
所以最后结果就是下面这样,最终的目的是从A
到 C
:
func >>><A,B,C>(_ f1: @escaping (A) -> B,
_ f2: @escaping (B) -> C) -> (A) -> C{
{
(v1: A) -> C in
f2(f1(v1))
}
}
柯里化
柯里化是函数编程中很重要的一个概念.我们先来看看什么是柯里化.
柯里化的定义:将一个接收多个参数的函数转变为一系列只接受一个参数的函数
很明显我们上面的运算操作就是将一个函数柯里化.
下面我们再练习一下分别将2个参数,3个参数的函数柯里化:
2个参数柯里化:
//两个参数柯里化
func add(_ v1: Int,_ v2: Int) -> Int{
v1 + v2
}
//柯里化后
func curringAdd(_ v1: Int) -> (Int) -> Int{
{$0 + v1}
}
3个参数柯里化:
//三个参数柯里化
func sub(_ v1: Int,_ v2: Int, _ v3: Int) -> Int{
v1 - v2 - v3
}
//柯里化后
func curringSub(_ v1: Int) -> (Int) -> (Int) -> Int{
return{
(v2) in
return{
(v3) in
return v1 - v2 - v3
}
}
}
但是上面的柯里化函数只支持Int
类型,如果我们想要通用其他类型,需要让他们支持泛型.这样我们就能把任何类型的函数自动柯里化.
将2个参数的柯里化函数泛型化:
func generics2arguments<A,B,C>(_ fn: @escaping (A,B) -> C) -> (A) -> (B) -> C{
return{
a in
return{
b in
return fn(a,b)
}
}
}
print(generics2arguments(add)(10)(20))
将3个参数的柯里化函数泛型化:
func generics2arguments<A,B,C,D>(_ fn: @escaping (A,B,C) -> D) -> (A) -> (B) -> (C) -> D{
return{
a in
return{
b in
return{
c in
fn(a,b,c)
}
}
}
}
print(curringSub(10)(20)(30))
这样我们就可以随便传入2个参数,3个参数的函数,然后自动将函数柯里化.
还可以将自动柯里化函数重载成运算符,使用的时候更方便:
prefix func ~<A,B,C,D>(_ fn: @escaping (A,B,C) -> D) -> (A) -> (B) -> (C) -> D{
return{
a in
return{
b in
return{
c in
fn(a,b,c)
}
}
}
}
这样即使+ - x /
传统的写法,也可以直接柯里化后参与运算:
// [(3 + 8) * 7 / 2] - 1
//传统写法,没有柯里化
func add(_ v1: Int, _ v2: Int) -> Int{
v1 + v2
}
func sub(_ v1: Int, _ v2: Int) -> Int{
v1 - v2
}
func multiple(_ v1: Int, _ v2: Int) -> Int{
v1 * v2
}
func divide(_ v1: Int, _ v2: Int) -> Int{
v1 / v2
}
prefix func ~<A,B,C>(_ fn: @escaping (A,B) -> C) -> (B) -> (A) -> C{
return{
b in
return{
a in
return fn(a,b)
}
}
}
infix operator >>> : AdditionPrecedence
func >>><A,B,C>(_ f1: @escaping (A) -> B,
_ f2: @escaping (B) -> C) -> (A) -> C{
{
(v1: A) -> C in
f2(f1(v1))
}
}
//直接使用 ~ 把传统方法自动柯里化后参与运算
var fn = (~add)(8) >>> (~multiple)(7) >>> (~divide)(2) >>> (~sub)(1)
print(fn(3))
函数式编程中常用的概念:
1. 高阶函数 Higher-Order Function
高阶函数是至少满足下列一个条件的函数:
1. 接收一个或者多个函数作为参数
2. 返回一个函数
通过高阶函数
的定义可以看出,上面的map , flatMap , reduce
等都是高阶函数,它们都是接收一个函数作为参数.
2. 柯里化 Currying
柯里化就是将一个接收多个参数的函数变成一系列只接受单个参数的函数.
柯里化的本质就是将一个接收多个参数的函数变成只接收一个参数,并且返回一个接收参数的闭包.通过返回的闭包达到接收多个参数的目的.
3. 函子 Functor
我们将支持map
运算的数据类型称为函子.
上面分析过map
函数,map
就是把数组中的值或者可选项包装的值,映射成另一个值后,然后再返回数组或者可选项,也就是返回他本身的数据类型.
如图:
// Array<Element>
func map<T>(_ transform: (Element) -> T) -> Array<T>
// Optional<Wrapped>
func map<U>(_ transform: (Wrapped) -> U) -> Optional<U>
Optional
的map
流程图:
Array
的map
流程图:
4. 适用函子 Applicative Functor
对于任何一个函子,如果能支持以下运算,那么它就是一个适用函子:
//传入任意类型,最后都能返回函子的数据类型.
func pure<A>(_ value: A) -> F<A>{
value
}
//参数一: 泛型函数 fn
//参数二: 函子
//功能: 把参数 value 传入 函数 fn , 最后得到一个 B,并且把 B ,包装成函子原本的数据类型
func <*><A,B>(_ fn: F<(A) -> B>, value: F<A>) -> F<B>
第一种pure
运算,Array
和Optional
都支持,因为我们传入任意类型,都能放到Optional
和Array
中.
第二种运算Optional
和Array
也同样支持.
我们首先看一下Optional
实现这种运算:
infix operator <*>: AdditionPrecedence
func <*><A,B>(_ fn: ((A) -> B)?, value: A?) -> B?{
guard let f = fn, let num = value else {return nil}
return f(num)
}
var num: Int? = 10
var fn: ((Int) -> Int)? = { $0 * 2 }
var result = fn <*> num
print(result) // Optional(20)
再使用Array
实现这种运算:
infix operator <*>: AdditionPrecedence
func <*><A,B>(_ fns: [((A) -> B)], values: [A]) -> [B]{
var arr: [B] = []
for i in fns.startIndex ..< fns.endIndex{
arr.append(fns[i](values[I]))
}
return arr
}
var fns = [{$0 + 1},{$0 + 2},{$0 + 3}]
var nums = [1,1,1]
let results = fns <*> nums
print(results) //[2, 3, 4]
Array
与Optional
不同的是,Array
是吧一些列的算法包装起来存放到数组中,然后分别取出每一个算法和每一个值进行运算,最后再把每一次的运算结果存放到数组中.
如图所示:
5. 单子 Monad
对于任意数据类型,如果支持以下运算,那么就可以称为是一个单子:
func pure<A>(_ value: A) -> F<A>
func flatMap<A,B>(_ value: F<A>,fn:((A) -> F<B>)) -> F<B>
Array
和Optional
支不支持这两种运算,它们是不是一个单子呢?
因为swift
官方文档中已经为Array
和Optional
提供了flatMap
的实现,我们看看官方实现:
Array.flatMap
的官方实现:
public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence
我们可以对其简化如下:
public func flatMap(_ transform: (A) -> Array<B>) -> Array<B>
官方实现简化后的代码和上面的算法已经很相似了,但是还少了一个参数,其实这个参数就是我们调用flatMap
的对象,因为上面的算法规则并不是面向对象的,所以我们可以把调用flatMap
的对象当做参数补充进去,如下:
public func flatMap(value: Array<A> , _ transform: (A) -> Array<B>) -> Array<B>
同样,对Optional
的flatMap
进行简化:
//官方API
@inlinable public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
//简化后
public func flatMap<A,B>(value: Optional<A>_ transform: (A) throws -> Optional<B>) -> Optional<B>
可以看到,和运算规则是一样的.
所以,Array
和Optional
也是单子.