从零学习Swift 16: 函数式编程

Swift语言是支持函数式编程的,所以我们需要简单了解一下函数式编程的概念.

在了解函数式编程的概念之前呢,先看看SwfitArray常用的几个方法,因为这几个方法在设计上都是按照函数式编程的规范去设计的.

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}

规则
mapflatMap

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的返回值是不管闭包返回值是什么,直接放到数组中.

mapcompactMap

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 ----")


optionalmap

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!)


Optionalmap 和 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)

flatMapmap的功能是一样的,只不过flatMap如果发现映射的结果本身就是可选项类型,那么它就不会再封装一层可选项;而map不管结果是什么都会再封装成可选类型.

知道了mapflatMap的区别.我们看看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的返回值,同时也是运算符的返回值,所以它俩是同一类型.

所以最后结果就是下面这样,最终的目的是从AC:


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>

Optionalmap流程图:

Optional

Arraymap流程图:

Array
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运算,ArrayOptional都支持,因为我们传入任意类型,都能放到OptionalArray中.

第二种运算OptionalArray也同样支持.
我们首先看一下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]

ArrayOptional不同的是,Array是吧一些列的算法包装起来存放到数组中,然后分别取出每一个算法和每一个值进行运算,最后再把每一次的运算结果存放到数组中.

如图所示:

Array 是把 +3 操作封装起来
5. 单子 Monad

对于任意数据类型,如果支持以下运算,那么就可以称为是一个单子:


func pure<A>(_ value: A) -> F<A>

func flatMap<A,B>(_ value: F<A>,fn:((A) -> F<B>)) -> F<B>

ArrayOptional支不支持这两种运算,它们是不是一个单子呢?

因为swift官方文档中已经为ArrayOptional提供了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>

同样,对OptionalflatMap进行简化:


//官方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>

可以看到,和运算规则是一样的.

所以,ArrayOptional也是单子.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335