Swift 柯里化

Swift 柯里化

前言:Swift中,柯里化在业务层的使用比较少,本文旨在介绍基本的柯里化的基本概念和基本使用,以备在读Swift源码或第三方源码时,碰到使用柯里化的地方时,便于理解。

进入正题之前之前我们先来看一段代码,代码功能就是简单地拼接String字符串:

class SomeClass {
    var str = ""
    func join(string: String) {
        str += str.isEmpty ? string : ", \(string)"
    }
}

接下来是调用:

var somCls = SomeClass()

// let closure: (String) -> ()
let closure = SomeClass.join(somCls)

closure("有没有")
closure("觉得")
closure("很奇怪??")

// 打印结果:有没有, 觉得, 很奇怪??
print(somCls.str)

从上述代码来看,功能代码并没有什么特别的地方,特别的一段代码是SomeClass.join(somCls),用类名调用实例方法,传入的参数是该类的实例,得到的一个匿名函数closure,类型是(String) -> ()

这是个什么东西?为什么可以这么用?又为什么要这么用?一连串的问题蹦出来了...

可以这么用,是因为SomeClass.join(somCls)是一个柯里化函数,接收一个参数,即当前类的实例,返回对应方法去除方法名的匿名函数,此类中的join方法,去除方法名,保留参数和返回值,不就是(String) -> ()

我们为SomeClass添加一个入参和返回值复杂一些的方法,然后使用柯里化的方式来调用,来验证我们的说明:

// 添加方法:
func difficult(param1: Int, param2: String, param3: Double) -> String {
    return "拼接:param1:\(param1), param2:\(param2), param3:\(param3)"
}

// 柯里化调用:
// let difClosure: (Int, String, Double) -> String
let difClosure = SomeClass.difficult(somCls)
// 打印结果:拼接:param1:12, param2:diff, param3:22.33
print(difClosure(12, "diff", 22.33))

// 正常调用
// 打印结果:拼接:param1:12, param2:diff, param3:22.33
print(somCls.difficult(param1: 12, param2: "diff", param3: 22.33))

difClosure的类型是(Int, String, Double) -> String,也就是difficult方法去除方法名的匿名函数吧,印证了上面说的。

使用相同的参数,柯里化调用和正常调用得到的结果是一样。

这样也是一样的:

// let closure: (String) -> ()
let closure = somCls.join
// let difClosure: (Int, String, Double) -> String
let difClosure = somCls.difficult

1、概念

柯里化(Currying),是一种函数式编程思想,就是把接受多个参数的函数转换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回一个接受余下参数的新函数技术。

所以,柯里化又可理解为部分求值(Partial Evaluation),返回接收剩余参数且返回结果的新函数。想要应用柯里化,就必须先理解柯里化的作用和特点,这里总结为三点:

  • 参数复用 – 复用最初函数的第一个参数
  • 提前返回 – 返回接受余下的参数且返回结果的新函数
  • 延迟执行 – 返回新函数,等待执行

Swift中,由于柯里化在业务层的应用较少,所以从Swift3.0之后移除了便捷柯里化的用法,但是特性还是保持了,就像上述那个例子,并且Swift的很多底层特性是使用柯里化来表达的,比如结构体的实例化方法就是一个柯里化函数。

2、柯里化函数的定义

tips:与类相关的函数,一般会称为方法,这里就直接都说函数了。

struct Currying {
    // 普通函数
    // 接收多个参数的函数
    func add(_ a: Int, _ b: Int, _ c: Int) -> Int {
        print("\(a) + \(b) + \(c) = \(a + b + c)")
        return a + b + c
    }
    
    // 系统 柯里化 函数
    // 柯里化函数,Swift 3.0 之前支持这样的语法,可以直接写
    //func addCur(_ a: Int)(_ b: Int)(_ c: Int) -> Int {
    //    print("\(a) + \(b) + \(c) = \(a + b + c)")
    //    return a + b + c
    //}

    // 自定义 柯里化 函数(系统的Swift 3.0之后不可用了)
    func addCur(_ a: Int) -> (_ b: Int) -> (_ c: Int) -> Int {
        return { (b: Int) -> (_ c: Int) -> Int in
            return { (c: Int) -> Int in
                print("\(a) + \(b) + \(c) = \(a + b + c)")
                return a + b + c
            }
        }
    }
}

我们要实现的功能是将传入的三个Int类型的参数累加后返回,普通函数定义好三个参数,函数体内将三个参数累加返回就完成了。

直接看已经定义好的柯里化函数,可能会有点不好理解,甚至会觉得很麻烦,觉得完全没有必要这么做,但这个例子只是演示将普通函数柯里化的过程,并没有发挥柯里化的优势。

柯里化是返回接收剩余参数且返回结果的新函数(闭包),接下来先拆解将普通函数转为柯里化函数的步骤:

  1. 函数定义:定义了一个接收第一个参数a,并返回一个接收参数b的函数,并且这个函数需要接收一个接收参数c,并返回Int类型的函数;表达式是这样的:入参(_ a: Int),返回值(_ b: Int) -> (_ c: Int) -> Int
  2. 第一层返回:返回一个接收第二个参数b的函数,并且这个函数需要接收一个接收参数c,并返回Int类型的函数;表达式是这样的:入参(_ b: Int),返回值(_ c: Int) -> Int
  3. 第二层返回:返回一个接收余下最后一个参数c,并且返回结果为Int类型的函数。表达式是这样的:入参(_ c: Int),返回值Int
  4. 第三层返回:做最后的计算,a + b + c的总和,然后返回。

tips:addCur函数体内为什么可以使用abc

  • 利用闭包的值捕获特性,即使这些值作用域不在了,也可以捕获到他们的值。
  • 闭包会自动判断捕获的值是地址引用还是值引用,如果修改了,就是地址引用,否则值引用。

注意只有在闭包中才可以,abc都在闭包中。

对于bc是在闭包中可以从代码中看出来,为什么a也是在闭包中呢?

我们使用柯里化来获取第一层的函数:

// let funcA: (Int) -> (Int) -> (Int) -> Int
let funcA = Currying.addCur(curryInstance)

funcA其实就是addCur柯里化后的第一层函数,其实也是一个闭包,闭包的第一个参数就是对应的就是addCur第一个参数a,所以a也是在闭包中的。

3、柯里化函数的调用

我们定义的函数addCur是一个实例函数,所以我们需要一个实例:

var curryInstance = Currying()
  • 一次性调用
var res = curryInstance.addCur(10)(9)(8)
// 打印结果:10 + 9 + 8 = 27

咋一看,这么调用觉得很奇怪。

嗯,我们对SnapKit肯定不陌生,我们在布局的时候,会有这样的写法maker.left.right.top.equalToSuperview(),这种写法是每.一次,然后都返回了maker,所以可以一直.下去,典型的函数式编程,而我们这里是连续调用闭包,因为每次都返回了一个新的闭包,然后再次调用这个返回的闭包。

文字的解释还是略显晦涩和难以理解,我们来拆解调用,就很清晰了。

  • 拆解调用

根据我们上述的柯里化函数的步骤,一步一步拆解:

// 分开调用 1 (把方法转成了匿名函数,保留入参和返回值)
// let funcA: (Int) -> (Int) -> (Int) -> Int
let funcA = Currying.addCur(curryInstance)

第一步,获取到一个接收Int参数(a),并返回一个接收Int参数参数(b)的函数,并且这个函数需要接收一个接收Int参数参数(c),并返回Int类型的函数。

// 分开调用 2
// let funcB: (Int) -> (Int) -> Int
// let funcB = curryInstance.addCur(10)
let funcB = funcA(10)

第二步,获取到一个接收Int参数参数(b)的函数,并且这个函数需要接收一个接收Int参数参数(c),并返回Int类型的函数。

得到第二个函数有两种方式:

  • 执行第一步得到的闭包,传入第一个参数a
  • 通过类的实例curryInstance调用addCur函数,传入第一个参数a

两者其实是一样的,只是步骤不同而已,得到的都是addCur函数的第一层返回,(Int) -> (Int) -> Int类型的函数。

// 分开调用 3
// let funcC: (Int) -> Int
let funcC = funcB(9)

第三步,获取到一个接收Int参数参数(c),并返回Int类型的函数。

// 分开调用 4
// let resC: Int
let resC = funcC(8)
// 打印结果:27
print(resC)

第四步,得到最终的a + b + c的结果,也就是本例中的10 + 9 + 8 = 27

4、Swift 标准库中的柯里化

Swift中实例方法就是一个柯里化函数。

上述的两个例子,有如下的调用:

var somCls = SomeClass()
// let closure: (String) -> ()
let closure = SomeClass.join(somCls)
// let difClosure: (Int, String, Double) -> String
let difClosure = SomeClass.difficult(somCls)

var curryInstance = Currying()
// let funcA: (Int) -> (Int) -> (Int) -> Int
let funcA = Currying.addCur(curryInstance)

可以通过类来获取实例方法,这就是Swift标准库对柯里化的支持。

通过柯里化来获取实例方法需要注意:

方法是什么类型,就会返回什么类型的函数,不过需要传入一个参数(类的实例)才能获取到,如果方法中有外部参数名,外部参数名也属于类型的一部分

这个怎么理解呢?我们来看调用这三个方法时的代码提示:

kelihua1.png
kelihua2.png
kelihua3.png

从代码提示中可以看出,直接使用类获取实例方法,有一个固定的入参,类的实例,然后返回对应的实例方法 —— 去除方法名保留入参和返回值的匿名函数。

5、柯里化的优势

通过以上的讲述,可以理解为当一个函数需要提前处理并需要等待执行或者接受多个不同作用的参数时候,便可应用柯里化。

柯里化其实是运用了函数式编程的思想,所以柯里化就具备函数式编程的特点和优势。

函数式编程编程的特点表现为以下两点:

  1. 只用“表达式”(表达式:单纯的运算过程,总是有返回值),不用“语句”(语句:执行某种操作,没有返回值)
  2. 不修改值,只返回新值

函数式编程编程的优势体现在以下几点:

  1. 代码简洁
  2. 提高代码复用性
  3. 代码管理方便,相互之间不依赖,每个函数都是一个独立的模块,执行单元测试便利
  4. 易于“并发编程”,因为不修改变量的值,都是返回新值

tips:函数式编程就不在这里拓展了,有兴趣深入了解的小伙伴Google一下,或者可以参考函数式编程初探。

6、柯里化的应用场景

以下举一些开发中能使用上的场景,以作抛砖引玉:

6.1 API拼接

常规套路,拼接API的完整链接,会写一个这样的函数:

func fetchURL(protocol: String, domain: String, path: String) -> String {
    return `protocol` + "://" + domain + "/" + path
}

这个函数使用三个参数拼接成完成的url,使用如下:

// http://test.api.shanzhu.com/1.0.0/goods.goodsList
let url1 = fetchURL(protocol: "http", domain: "test.api.shanzhu.com", path: "1.0.0/goods.goodsList")
// http://test.api.shanzhu.com/1.0.0/config.configData
let url2 = fetchURL(protocol: "http", domain: "test.api.shanzhu.com", path: "1.0.0/config.configData")

然后我们发现,前两个参数是固定的,并不需要每次都传递,所以优化一下:

func fetchURL(protocol: String = "http",
              domain: String = "test.api.shanzhu.com",
              path: String) -> String {
    return `protocol` + "://" + domain + "/" + path
}

上述的调用也就可以优化成这样子:

// http://test.api.shanzhu.com/1.0.0/goods.goodsList
let url1 = fetchURL(path: "1.0.0/goods.goodsList")
// http://test.api.shanzhu.com/1.0.0/config.configData
let url2 = fetchURL(path: "1.0.0/config.configData")

不过呢,也还是有一丢丢小问题的,如果仅仅是自己使用的话,这样也就足够了,不过如果要作为一个通用的函数供其他人使用,会有很多其他的情况,比如,某个人需要使用https了,每个环境会有不一样的domain,给参数设置默认值,仅仅能解决的是某一个问题,跳出这个问题,又回到了传三个参数的调用方式。

针对这种情况,柯里化可以完美解决:

func fetchURL(_ protocol: String) -> (_ domain: String) -> (_ path: String) -> String {
    return { (_ domain: String) -> (_ path: String) -> String in
        return { (_ path: String) -> String in
            return `protocol` + "://" + domain + "/" + path
        }
    }
}

通过柯里化函数,我们获取一个测试环境、一个预发环境和两个生产环境的API

// http://test.api.shanzhu.com/1.0.0/goods.goodsList
let testAPI = fetchURL("http")("test.api.shanzhu.com")("1.0.0/goods.goodsList")

let https = fetchURL("https")

// https://pre.api.shanzhu.com/1.0.0/order.list
let preAPI = https("pre.api.shanzhu.com")("1.0.0/order.list")

let prd = https("prd.api.shanzhu.com")

// https://prd.api.shanzhu.com/1.0.0/main.other
let prdAPI1 = prd("1.0.0/main.other")
// https://prd.api.shanzhu.com/1.0.0/config.configData
let prdAPI2 = prd("1.0.0/config.configData")

从调用中,我们发现了一个很有趣的事情:

  • 1、函数的调用是可以分段进行的

    在获取最终的结果前,我们将函数一次性调用(testAPI的获取),也可以分为两段调用(preAPI的获取),也可以分为三段调用(prdAPI1的获取),但顺序是严格按照函数的参数顺序来的。

  • 2、分段调用的返回值可以复用

    分段函数均是可以复用的,例如对第一段调用的返回值https的复用,第二段调用的返回值prd的复用。

我们可以思考一下,如果是常规函数调用,如果一次性要获取这些API,代码是什么样子的?

在这个例子中,柯里化帮我们做的事情,相对于传统调用,减少了重复传递不变的部分参数

当然,这里也同样有我们需要注意的地方:

  1. 柯里化函数的参数必须按顺序调用,所以我们必须安排好参数的顺序。重复率高的参数放在前面例,如本例中的网络协议protocol和域名domain,重复率低的放在后面,如本例中的地址path
  2. 柯里化函数代码只会调用一次,比如let https = fetchURL("https")时调用到了return { (_ domain: String) -> (_ path: String) -> String in这一行,那么在let prd = https("prd.api.shanzhu.com")就不会再次调用这一行,而是执行return { (_ path: String) -> String in,直到prd("1.0.0/main.other")这个执行时,才会执行returnprotocol+ "://" + domain + "/" + path,可以打断点试一下哈~

通过这个例子,柯里化函数,有种工厂方法的感觉,只不过他生产出来的不是类或UI,而是各种各样底层相同,部分参数重复率高的函数。比如我们举的这个例子,方法的底层都是对三个参数的拼接,而网络协议和域名,是重复率高的参数。

6.2 构建分层UI

我们有个需求,需求中有很多view,分析过后发现,这些view的上面部分都是一样的,下面部分有三种不同的样式。

常规套路,我们都会使用继承来实现,首先建一个父类,写上上面相同部分的代码,然后建出三个子类,以实现对应的三种样式。

我们用柯里化来实现以下这个功能的简化代码:

protocol CreateUIProtocol {
    func combine(_ top: (UIView) -> ()) -> (_ bottomType: Int, _ bttom: (UIView) -> ()) -> ()
}
class UI: CreateUIProtocol {
    func combine(_ top: (UIView) -> ()) -> (_ bottomType: Int, _ bttom: (UIView) -> ()) -> () {
        top(self.cearteTop())
        return { [weak self] (_ bottomType: Int, _ bttom: (UIView) -> ()) -> () in
            guard let `self` = self else { return }
            bttom(self.cearteBottom(bottomType))
        }
    }
    func cearteTop() -> UIView {
        return UIView()
    }
    func cearteBottom(_ type: Int) -> UIView {
        let v = UIView()
        v.tag = type
        return v
    }
}

然后我们来绘制这三份UI

let ui = UI()

let topCreate = UI.combine(ui)
let bottomCreate = topCreate {
    print("createTopUI finished~")
    print($0)
}
bottomCreate(1) {
    print("createBottomUI finished~")
    print($0)
}
bottomCreate(3) {
    print("createBottomUI finished~")
    print($0)
}
bottomCreate(6) {
    print("createBottomUI finished~")
    print($0)
}

// 打印结果:
createTopUI finished~
>
createBottomUI finished~
>
createBottomUI finished~
>
createBottomUI finished~
>

整个看下来,这个例子与API拼接的例子大同小异,功能虽然不一样,但原理是一样的。

1、由于顶部是一样的,所以只需要绘制一次就可以了;
2、然后分别绘制三份底部不一样的部分。

7、结语

通过上述的了解,将来在研究Swift标准库的源码和第三方库的源码时,再碰到柯里化函数,就不会陌生和难以理解了,也是本文的初衷。

柯里化函数在实际开发中的使用频率不高,如果有其他契合使用的场景,希望不吝赐教,共同进步。

--

参考文档:

https://www.jianshu.com/p/f61ef47d7bb6
https://www.jianshu.com/p/6eaacadafa1a
https://segmentfault.com/a/1190000015281061
https://www.cnblogs.com/duiniweixiao/p/8919695.html

你可能感兴趣的:(Swift 柯里化)