1 概念
函数式编程,简称FP(Functional Programming)。
1.1 数学含义
y=f(x)
,用FP
的思想解释:主体是“f”(函数,图上的箭头),参数是x,y。
参数,可以是对象,也可以是函数
高阶函数:函数的参数也是函数
1.2 VS 对象
面向对象 --> 狗.吃(食) | FP --> 吃(狗,食) | |
---|---|---|
示例 |
|
|
主体 | 对象 --> 狗 | 函数 --> 吃 |
描述 | 对象“狗” has a method “吃” with param "食" | function “吃” with params:狗、食 |
特点 | 函数不能单独存在,必须被声明在class里 | 函数可以独立存在 |
函数和对象,不存在谁从属于谁,只是思维方式的区别。
“对象”,会把混沌的业务拆分成若干固定的模块(架构),然后,再细化。理想情况下,模块设计好后,在实施的过程中,即便出现严重的BUG,也只影响有限的模块。当然,这很难做到。于是,需要持续重构系统。如果模块设计地太粗糙,还需要推倒重来。所以,从对象的角度出发,很自然地会用到很多设计模式,从宏观的角度思考问题,避免代码大范围地糜烂。
函数式思维倾向于把一件事做到极致,恰好可以弥补“对象”的不足。
1.3 编程语言的历史
现代语言通过一层又一层的抽象,封装了琐碎的细节。比如,Java语言的垃圾收集器。每一层抽象,都在吃硬件。
普通的方法,传递的“对象”是“静态”的,而高阶函数传递的“函数”是“动态”的。而且,函数嵌套的代码,可阅读性也差、性能也差、学习曲线陡峭...但是呢,“函数”是把利刃,尤其是在具体的业务场景里,用好“函数”,可以快速解决问题。如果,还是继续用Java的“对象”,或是Java8的lambda,估计人家喝咖啡的时候,你还在加无聊的班。
在kotlin之前,我也用Groovy、Scala写过函数,尤其是Scala。相信我,kotlin的语法更严谨。
函数,用kotlin写啦。没有书么?自己写呢?
2 知识点
2.1 闭包
所有函数都是闭包(忘了在哪里看到的了。感觉差不多,你说呢)
2.1.1 调用外部变量
变量的作用域:全局、局部。
在闭包内部的变量是“局部变量”,但,在闭包内部可以调用外部的变量。代码如下:
context("外部变量number = 1") {
val number = 1
on("在闭包中调用外部变量number -- 简化的写法") {
fun f1() = { number + 1 }
it("should return 2") {
assertEquals(2, f1()()) //很奇怪?别急,继续看下面的代码
}
}
on("调用number -- 展开所有的类型") {
//“()”:没有参数
//返回值"() -> Int",是个函数
fun f1(): () -> Int = { number + 1 }
it("should return 2") {
val alsoFunction: () -> Int = f1()
val value = alsoFunction()
assertEquals(2, value)
}
}
}
在JavaScript中,变量声明的时候,如果不用var修饰,默认是全局的...再次吐槽JavaScript。kotlin没有这个BUG。代码如下:
on("闭包内部的变量"){
fun f1():() -> Int = {
val number = 1
number
}
//println(number) 会报错。
it("should be 1") {
assertEquals(1, f1()())
}
}
2.1.2 读取闭包的内部变量
示例有点难。能看懂,还是会有收获的
on("读取闭包的内部变量") {
fun makeCounter(): () -> Int { //返回值也是函数
var backing = 0 //声明函数内部的变量
return fun(): Int { return ++backing } // 匿名函数,可以访问外部变量backing
}
it("should all be passed") {
val counter = makeCounter()
assertEquals(1, counter())
assertEquals(2, counter())
assertEquals(3, counter()) //每次计算,backing都会加1
//顺便学习invoke的用法
assertEquals(1, makeCounter().invoke())
assertEquals(1, makeCounter()())
}
}
-
场景描述
临时需要一个计数器,而且,有可能多次使用?
-
Java代码示例
class Counter{ private int backing = 0; public int getCount(){ return ++backing; } }
-
Kotlin的另一种实现(介于函数和对象之间)
delegate,用在这个例子里,有点过了。只是推导哈,验证一些想法,还是有学习的意义的
on("use delegate 读取闭包的内部变量") { var counter: Int by object { private var backing: Int = 0 operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = ++backing operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { backing = value } } it("should all be passed") { assertEquals(1, counter) assertEquals(2, counter) assertEquals(3, counter) } }
2.1.3 函数与闭包
再看几个例子,还是有点难。
describe("闭包的应用") {
data class Employee(val name: String, val salary: Int) //测试用的model
context("返回值是闭包") {
fun paidMore(amount: Int): (Employee) -> Boolean = { it.salary > amount }
context("使用闭包,设定标准low level:> 100") {
val amount = 100
val isHighPaid = paidMore(amount) //请注意:还是函数
it("should all be passed") {
assertTrue(isHighPaid(Employee("yuri", amount + 1)))
assertFalse(isHighPaid(Employee("yuri", amount - 1)))
}
}
}
context("参数是闭包,且有默认的实现") {
//Employee.paidMore,kotlin的扩展函数。又扯远了。先尝尝吧
//这种写法相当于元编程,给Employee添加了函数paidMore
fun Employee.paidMore(amount: Int, predicate: (Int) -> Boolean = {
this.salary > it //this特指Employee
}): Boolean = predicate(amount)
on("use the default implementation") {
val amount = 100
fun Employee.isHighPaid() = paidMore(amount)
it("should all be passed") {
assertTrue(Employee("Alice", amount + 1).isHighPaid())
assertFalse(Employee("Alice", amount - 1).isHighPaid())
}
}
on("override the default implementation") {
val amount = 100
fun Employee.isHighPaid() = paidMore(amount) {
this.salary > it * 2
}
it("should all be passed") {
assertTrue(Employee("Bob", amount * 2 + 1).isHighPaid())
assertFalse(Employee("Bob", amount * 2 - 1).isHighPaid())
}
}
}
}
2.1.4 总结
- 简单、有效
简单的场景,专门定义一个class & method,感觉有点脱了裤子放屁的意思。
如果业务场景经常变,就需要封装变化的部分了。
注意:你得区分变化的是“结构”还是“细节”
如果是“流程”、“结构”变化了,你需要create new classes
如果是具体的“算法”、“逻辑”,你需要函数、闭包、高阶函数
- 闭包占用额外的内存
闭包是动态的,通过 2.1.2 的例子,你会发现,闭包的状态一直都在内存里。所以,对性能有特殊要求的场景,你需要对闭包专门调优。
- 代码阅读性差
使用闭包后,代码可以写得很花,但是,阅读的时候就费劲了。相比groovy、Scala,kotlin的语法已经严谨很多了,至少,回头看自己写的代码,花点时间还是能看懂地。至于阅读的速度,自己练吧。
当然,还有函数和闭包的区别,这个...不要计较了,会用就行。
2.2 偏函数
其中,g(y,z)是“偏函数”
given("闭包: (Int,Int) -> Int") {
val add = { x: Int, y: Int -> x + y }
it("should all be passed") {
fun addY(y: Int) = add(1, y) //指定add的参数x=1,得到新的函数addY
assertEquals(3, addY(2))
}
}
2.3 柯里化 (Carrying)
那么,kotlin支持柯里化吗?且看下面的例子:
describe("柯里化") {
given("柯里化的闭包: (Int) -> (Int) -> Int") {
val add = { x: Int ->
{ y: Int ->
x + y
}
}
it("should all be passed") {
assertEquals(3, add(1)(2))
val addY = add(1)
assertEquals(3, addY(2))
}
}
given("不能柯里化的闭包: (Int,Int) -> Int") {
val add = { x: Int, y: Int -> x + y }
it("should all be passed") {
assertEquals(3, add(1, 2))
fun addY(y: Int) = add(1, y)
assertEquals(3, addY(2))
}
}
given("柯里化的函数:(Int) -> (Int) -> Int") {
fun add(): (Int) -> (Int) -> Int = { x: Int ->
{ x + it }
}
it("should all be passed") {
assertEquals(3, add()(1)(2))
val addY = add()(1) //柯里化+偏函数
assertEquals(3, addY(2))
}
}
given("不能柯里化的函数:(Int, Int) -> Int") {
fun add(): (Int, Int) -> Int = { x: Int, y: Int -> x + y }
on("直接调用add函数") {
it("should be equal") {
assertEquals(3, add()(1, 2))
}
}
on("使用偏函数,模拟柯里化的效果") { //瞎玩,貌似也没什么效果
fun addY(y: Int) = add()(1, y) //指定原函数add的参数x=1,得到新的函数addY
it("should also be equal") {
assertEquals(3, addY(2))
}
}
}
}
kotlin不支持柯里化,为什么呢?