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
也是單子.