目标主要是为了弄清楚:
● 使用函数式风格来编程的意义是什么?
● 为什么我要将函数作为参数传递?我定义一个接口,让他们来调用不就好了?我为什么要把函数作为一个值
函数是我们从小到大就在数学中接触的概念,在数学课本中函数的定义是这样的:
给定一个数集A,假设其中的元素为x,对A中的元素x施加对应法则f,记作f(x),得到另一数集B,假设B中的元素为y,则y与x之间的等量关系可以用y=f(x)表示,函数概念含有三个要素:定义域A、值域B和对应法则f。其中核心是对应法则f,它是函数关系的本质特征。
也就是定义域到值域的映射关系。 例如 successor(x) = x + 1
的函数表达的正整数和输出结果的关系, 函数名称叫 successor
。
在数学中, 对函数起名字并不是一个必要的工作,只是为了方便使用它,这没有错,但是函数名称和函数定义是不存在强关系的, successor
也可以被其他名字替换, 它本身不代表什么。
假定 A 是定义域, B 是值域, 在 Kotlin 的语法中则可以表示为 : (A) -> B
, 其反函数(求导) 则为 (B) -> A
下面有两个条件
满足 A & B
的函数称为 “全函数”, 而 !A & B
的称为 “偏函数”
严格意义上来说 , 偏函数不是函数,全函数才是真男人。
为什么我们要知道这个看起来很冷的词汇呢? 这是因为在编程中,许多错误就是开发者把偏函数当成全函数来使用。
例如 f(x) = 1/ x
是一个 N 到 Q 的偏函数, 因为 定义域没有对 0 进行进行定义, 所以当输入 x = 0 时,会输出错误,这是不符合函数的预期的。
而 f(x) = 1 / x , x属于N*
或 f(x) = 1 / x , 值域为 Q 或错误值(N 到 Q或错误值)
则是全函数, 我们输入 x = 0 时,要么是正确结果,要么是符合预期的错误结果。
将 偏函数转化成全函数是安全编程的一个重要部分!!!!!, 而上面转化成全函数的两种做法也正是我们常用的处理偏函数的常用方式,即:
在编程中, 我们的函数经常看起来不止有一个入参,例如 f(x, y) = x * y
,而函数的定义是 一个源集 到 一个目标集 的关系,那这还是函数吗?
答案是肯定的,我们引入了 元组(tuple) 这个特例概念, 即 (x, y) 甚至 (x, y, z) 都是一个元组,这个元组的定义域就是源集。所以是没有 多参数函数 这种概念的。
柯里化函数是对上面所讲的 元组函数 的变形。
假定 f(x, y) = x + y
,那么有以下逻辑推演:
因为 f(x, y) = x + y 定义域为 N, 值域为 N
假定 g(x) = h 定义域 x 属于 N, 值域为 自变量x 的 映射函数h
假定 h(y) = x + y 定义域为 N, 值域为 N
因为f函数和g函数的定义域、值域都相同, 是 N 到 N,且最终结果表达式为 x + y
所以 f(x, y) = g(x)(y)
将 g 函数改名为 f
得出 f(x, y) = f(x)(y)
(上面的推演是我自己写的,数学好的大佬不要骂我,我就是这样蛊惑自己的哈哈哈哈啊哈哈哈哈哈)
f(x)(y)
就是 f(x, y)
的柯里化形式, 数学中称这种函数为柯里化函数
这个概念很好理解,它是在柯里化函数上进行深化的。
例如函数: f(x, y) = x + y
那么它等价于 f(x)(y)
那 f(x)
是什么呢? 它代表的是自变量 x 对应的映射函数 , 当 x = 1时,f(1) 的结果 是一个函数,这个函数:输入是 y, 结果是 y 加上这个1。 那么我们称 f(x)
是f(x, y)
对x的偏应用函数 ,偏应用函数会对自变量计算产生很大的影响,这个我们会在后面讲到。
在 Kotlin 中:
val x = 5
, 可以看成是一个 f(x)=5 的函数,就是自变量无关的一种特殊函数前面讲过数学中的全函数, 在 Kotlin 中,程序员创造了一个与之相似的概念,叫 “纯函数”, 这是因为虽然编程语言定义了 fun
关键字来声明函数,但是很多时候,程序员所写的函数很少能称为真正的函数,所以提出这个概念,愿景是希望Kotlin开发者能够多写真正的函数。
函数要成为纯函数的条件如下:
请看下面代码的 1~9 的方法, 想想哪些函数是纯函数:
class Sample {
var percent1 = 5
private var percent2 = 9
val percent3 = 13
fun add(a: Int, b: Int): Int = a + b // 1
fun mult(a:Int, b: Int?): Int = 5 // 2
fun div(a: Int, b: Int): Int = a / b // 3
fun div(a: Double, b: Double): Double = a / b // 4
fun applyTax1(a: Int): Int = a / 100 * (100 + percent1) // 5
fun applyTax2(a: Int): Int = a / 100 * (100 + percent2) // 6
fun applyTax3(a: Int): Int = a / 100 * (100 + percent3) // 7
fun append1(i: Int, list: MutableList<Int>): List<Int> {
list.add(i)
return list
} // 8
fun append2(i: Int, list: List<Int>) = list + i // 9
}
第一个:纯函数
第二个:纯函数, 而且是常函数
第三个:不是纯函数, 因为当 b == 0
时, 程序会抛出错误
第四个:是纯函数,因为当 b == 0.0
时,会返回 Double.Infinity
第五个:不是纯函数, 因为 percent1
是公有的, 在两次调用该函数的期间, 这个 percent1
有可能会被外界改变,所以函数可能会返回不一样的值
第六个:是纯函数, 虽然依赖的 percent2
是可变的,但是该类中没有其他地方去改变这个值,而且因为它是私有的所以它不能被外界所改变。但是这种是不安全的,推荐将 percent2 改成 val 来声明
第七个:对于 参数 a 是纯函数,因为 percent3
是不可变的
第八个:非纯函数, 它改变了入参 list
的内容
第九个:纯函数,因为 list + i
返回的是一个新的 List 对象
Kotlin 是允许将函数写成数据的
例如 下面的函数:
fun add(a: Int, b: Int): Int = a + b
等价于:
val sum :(Int, Int) -> Int = {a, b -> a + b}
这里用了 lambda 表达式,这里就不再赘述,之前学习过:Lambda学习
在 Kotlin 中,函数有两种定义形式,一种是通过 fun
关键字定义,一种是使用 值 来定义,他们的区别是什么呢?为什么不像 Java 那样只用一种方法来定义呢?
fun
fun
关键字声明的函数,更优效率,并且更加美观fun
声明的函数也可以做为对象传递, 使用 ::funName
的形式 )下面我们将两个函数进行复合, 我们不仅要学习拆分函数的,也需要学会聚合函数,例如下面的两个函数:
// 复合下面两个函数,做到先乘3,再开平方
fun square(n: Int) = n * n
fun triple(n: Int) = n * 3
可能第一眼,会这样: val result = square(triple(3))
但这时不是真正的复合函数,只是复合了函数的应用。
下面的答案是以函数编程来进行复合的:
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { f(g(it)) }
然后我们就可以通过函数引用的方式,来进行复合:
val squareOfTriple = compse(::square, ::triple)
val result = squareOfTriple(3)
如果想要把 compose 变得更加强大和通用,可以加入泛型, 如下所示:
fun <T, U, V> compose(f: (U) -> V, g(T) -> U): (T) -> V = { f(g(it)) }
这样,我们就把 compose 的功能变得很强大了, 使用泛型,能匹配任何类型的 compose 函数。
上面一章学了函数的概念, 但是还没有解答一个最基本的问题,即为什么要将函数作为数据,进行使用或者传递,为什么不只是用 fun 版本? 下面需要来考虑处理多参数的函数。
没有多个参数的函数, 只有多个参数组成的元组的函数, 就是元组的参数可以是任意多个, 它本身可以是 Pair
或者 Triple
类型等。
现在定义一个函数, 由两个整数相加, 将函数作用于第一个整数,然后返回一个函数, 这个函数的类型是 :
(Int) -> (Int) -> Int
那么这个整数相加的函数就是:
val add: (Int) -> (Int) -> Int = {a -> {b -> a + b}}
或者使用 typeAlias 使用类型别名:
typelias IntBinOp = (Int) -> (Int) -> Int
val add: IntBinOp = {a -> {b -> a + b}}
那该如果使用 add 函数来将 3 和 5 相加呢,那就要用到上面学习的柯里化函数了, add 函数被认为是等效的元组函数val add: (Int, Int) -> Int = { a, b -> a + b }
的柯里化形式,使用为:
val result = add(3)(5)
在上一章中,我们为了达到函数复合 ,编写了一个 compose
函数,这种函数接收两个函数组成的元组,作为其参数,并返回一个函数。 但其实可以用值函数来代替 fun 函数, 这种特殊类型的函数, 以函数为参数并返回函数,称之为高阶函数HOF。 下面我们将 compose 函数( Int 版本), 写成值函数的形式:
先来看看这个 compose 的类型:
因为它原本是 add(f: (Int) -> Int, g(Int) -> Int): (Int) -> Int
所以可以看成是:
((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int
那么完整代码是:
val compose: ((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int = {x -> {y -> {z -> x(y(z)) }}}
其中 x 是第一个参数函数, y 是第二个参数函数, z 是入参, 函数将 y(z) 的结果应用到 x 函数上
或者使用别名:
typealias IntUnary = (Int) -> Int
val compose: (IntUnary) -> (IntUnary) -> IntUnary = {x -> {y -> {z -> { x(y(z)) }}}
最后使用:
val square: IntUnary = { it * it }
val triple: IntUnary = { it * 3 }
// 这里注意下顺序
val squareOfTriple = compose(square)(triple)
上面的 compose 只能符合 Int 到 Int , 我们可以使其多态化,让其复合多种不同的类型,为此我们加入泛型。下面来编写一个多态版本的 compose 值函数,看起来我们只要将上面的 Int 换成泛型就行了?如下
val <T, U, V> higherCompose: ((U) -> V) -> ((T) -> U) -> (T) -> V = { f ->
{ g ->
{ x ->
f(g(x))
}
}
}
但是这样是不行的,因为 Kotlin 不允许对属性使用泛型, 如果你要使用泛型,只能在类、接口和fun函数上,所以我们只能把其改成 fun 函数:
fun <T, U, V> higherCompose(): .....
也可以写成:
fun <T, U, V> higherCompose() = { f: (U) -> V ->
{ g: (T) -> U ->
{ x: T ->
f(g(x))
}
}
}
higherCompose
不接收任何函数,并且始终返回相同的值,是一个常函数。接下来使用它时,必须要指明泛型类型,告诉编译器当前函数使用的类型,不然编译会报错:
val squareOfTriple = higherCompose<Int, Int, Int>()(square)(triple)
下面来编写一个 higherCompose
, 使得 higherCompose(f)(g)
等价于 higherCompose(g)(f)
,很简单,交换下 f 和 g 的类型即可:
fun <T, U, V> higherComposeAndThen() = { f: (T) -> U ->
{ g: (U) -> V ->
{ x: T ->
g(f(x))
}
}
}
这样的目的是测试参数的顺序, 使用从 Int 到 Int 的函数进行测试将是模棱两可的,因可以按两种顺序符合函数,这样很难检测出错误,我们在测试时,可以使用多种类型
可以看看这样的测试代码:
val f: (Double) -> Int = { (it*3).toInt() }
val g: (Long) -> Double = { it + 2.0 }
assertEquals(Integer.valueOf(9), f(g(1L)))
assertEquals(Integer.valueOf(9), higherCompose<Long, Double, Int>()(f)(g)(1L))
我们可以使用匿名函数来省略中间函数的定义,例如:
val f: (Double) -> Double = { Math.PI / 2 - it }
val sin: (Double) -> Double = Math::sin
val cos: Double = compose(f, sin)(2)
可以使用匿名函数写成:
val cosValue: Double = compose({ x: Double -> Math.PI / 2 - x }, Math::sin)(2.0)
也可以使用高阶函数写成:
val higherCosValue = higherCompose<Double, Double, Double>()({ x: Double -> Math.PI / 2 - x })(Math::sin)
可以换成Kotlin官方推荐的Lambda表达式写法:
val higherCosValue = higherCompose<Double, Double, Double>()() { x: Double -> Math.PI / 2 - x }(Math::sin)
一般来说,匿名函数还命名函数,选择是任意的, 通常下,如果一个函数只使用一次,可以把这个函数弄成匿名函数
看下下面代码:
val taxRate = 0.09
fun addTax(price: Double) = price + price * taxRate
上面的 addTax
对 price
来说不是一个纯函数,因为函数依赖了参数以外的属性 taxRate
, 对于同样的price,它可能会返回不同的结果(尽管 taxRate 是使用 val 来声明的)。 只能说该函数是元组 (price, taxRate)
的纯函数。
所以当函数作为参数传递给其他函数时,它们可能会引发问题。 如果这类函数在同一个类出现很多次,这会使得程序难以阅读或者维护。
为了使得函数易于阅读和维护,一种方法是使他们更加的模块化,这使得程序的每个部分都可以单独的作为一个模块来使用,我们可以通过把元组作为参数来实现:
val taxRate = 0.09
fun addTax(taxRate: Double, price: Double) = price + prie * tax
上面学了多参数处理,所以也可以写成值函数版本或者柯里化版本…
// 值函数 + 闭包
val addTax = {taxRate: Double, price: Double -> price + price * taxRate }
// 柯里化 + 闭包
val addTax = {taxRate: Double ->
{price: Double ->
price + price * taxRate
}
}
上面写了闭包类型和柯里化类型,虽然对于同样的入参,他们的结果是一样的,但是他们的语义是不一样的。
闭包是一股脑的将参数塞入,而柯里化则是层层递进。
上面的柯里化版本,其实就等价于下面的类:
class TaxComputer(private val rate: Double) {
fun compute(price: Double): Double = price + price * rate
}
代码:
val tc9 = TaxComputer(0.09)
val result = tc9.compute(12.0)
等价于柯里化的:
val tc9 = addTax(0.09)
val resulte = tc9(12.0)
可以看到,其实柯里化函数和偏应用函数是密切相关的,可以做到一个参数接一个参数将一个元组给替换为可偏应用的函数。这就是其和元组参数的区别。
试着写一个函数, 双参的柯里化函数,偏应用其第一个参数,推演如下:
假设双参为 A、B,函数返回参数为 C, 那么该函数的类型为:
fun <A, B, C> originFun(): (A) -> (B) -> C
偏应用第一个参数,也就是输入参数A, 得出的结果是一个 (B)-> C 的函数, 可以得到如下的类型:
fun <A, B, C> partialA(a: A, f:(A) -> (B) -> C): (B) -> C
答案很简单,就是将第二个参数应用到第一个参数上:
fun <A, B, C> partialA(a: A, f:(A) -> (B) -> C): (B) -> C = f(a)
试着写一个函数, 也是双参的柯里化函数,偏应用其第二个参数,推演如下:
同样的使用上面的 originFun:
fun <A, B, C> originFun(): (A) -> (B) -> C
第二个参数是 B, 那么要求输入一个B, 得到一个函数为 (A)->C
fun <A, B, C> partialB(b: B, f:(A) -> (B) -> C): (A) -> C
因为 变量是一个 a ,所以可以这样开始:
fun <A, B, C> partialB(b: B: f:(A) -> (B) -> C): (A) -> C = { a ->
f(a)
}
f 函数还需要一个b,因为b已经在参数里面了, 所以答案就是:
fun <A, B, C> partialB(b: B: f:(A) -> (B) -> C): (A) -> C = { a: A ->
f(a)(b)
}
写一个函数来将柯里化 (A, B) -> C
类型的函数
已知类型为 fun <A, B, C> origin(a: A, b: B) -> C
那么可以接受这个函数, 并且返回一个柯里化形式的函数:
fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C = { a->
{b ->
f(a, b)
}
}
假如一个函数有两个参数,但有时候我们不想直接得到结果,(例如其中一个参数还不清楚值),我们只想通过一个参数来获得一个偏应用函数,例如下面的:
val addTax: (Double) -> (Double) -> Double = { x->
{y ->
y + y/100 * x
}
}
开发者可能想先计算税,然后获得一个参数的新函数,然后可以将该函数应用于任何价格:
val add9percentTax: (Double) -> Double = addTax(9.0)
然后我们用这个函数来加税了
val priceIncludingTax = add9percentTax(12.0)
但是对于 addTax
函数来说, 如果两个参数变化了位置,即第一个价格,第二个是税率,这个函数又是别人写的,对我们来说我们现在只知道税率,不知道价格, 我们该怎么办才好呢?
答案是我们可以写一个 fun 函数来交换柯里化函数的参数:
fun <T, U, V> swapArgs(f:(T) -> (U) -> V): (U) -> (T) -> V = {u: U ->
{t: T ->
f(t)(u)
}
}
前面的示例虽然使用了 Int、String、Double 等基础类型来表示价格、税率等业务实体,这也是我们编程中常用的做法,但是它会导致一些问题,我们应该相信类型而不是名字, 例如 称 Double 值为 “price” 并不意味它是一个价格, 这只是表明了一种意图。称另个一个 Double 值 叫 “taxRate” ,则表示了另一个意图。
为了使程序更安全,我们需要使编译器可以检查更广范围的类型,这可以避免将 “price” 和 “taxRate” 相加,在编译器中,两个 Double 值相加是没问题的,但实际上是错误的。
下面来看一个代码例子,来看待这种问题。 假设一个商品有名称、价格、重量,需要创建商品销售的发票,这些发票必须注明商品、数量、总价和总重量。
下面用一个 Product 来表示一个商品:
data class Product(val name: String, val price: Double, val weight: Double)
然后,用一个 OrderLine 来表示一个订单的每一行:
data class OrderLine(val product: Product, val cnt: Int) {
fun weight() = product.weight * cnt
fun amount() = product.price * cnt
}
继续使用标准类型, 使用 List
来表示订单, 下面我在main方法中来处理订单的价格和数量,代码如下:
fun main() {
val iPhone = Product("iPhone", 3.5, 0.5)
val xiaomi = Product("xiaomi", 1.5, 0.2)
val orderLines = listOf(OrderLine(iPhone, 2), OrderLine(xiaomi, 3))
val weight = orderLines.sumByDouble { it.amount() }
val amount = orderLines.sumByDouble { it.weight() }
}
可以看到,编译是不会报错的, 也有运行结果, 但是这个结果显然是错误的。
建模中有一个概念: 类不应该有多个相同类型的属性, 相反, 他们应该有一个具有特定基数的属性, 这很好的提醒了我们,如果在类中定义了几个相同类型的属性,则需要小心这些属性被弄混
为了避免上面出现的问题, 应该使用 值类型, 例如一个类来表示价格:
data class Price(val value: Double)
data class Weight(val weight: Double)
但这并不能解决问题,因为也可以写成:
val total = price.value + weight.value
接下来需要做的,就是为 Price 和 Weight 定义加法, 可以通过 operator
关键字来声明:
data class Price(val value: Double) {
operator fun plus(price: Price) = Price(this.value + price.value)
operator fun times(num: Int) = Price(this.value * num)
}
data class Weight(val value: Double) {
operator fun plus(weight: Weight) = Weight(this.value + weight.value)
operator fun times(num: Int) = Weight(this.value * num)
}
这样编译器就能帮我们检查到错误了。
现在, 不再使用 sumByDouble 来计算 price 的总和, 因为它只检查 Double 类型。 我们可以使用更好的方法: fold
或者 reduce
,它们可以将集合缩减为单个元素, 两者区别不是很明显,通常取决于:
下面将使用 fold ,会使用一个为 0 的价格 Prioce(0.0) 以及一个为 0 重量 Weight(0.0) 作为初始值,然后作为参数的函数则使用刚刚定义的加法,可以使用 Lambda 表达式,代码如下:
// 修改 OrderLine 的函数
data class OrderLine(val product: Product, val cnt: Int) {
fun weight(): Weight = product.weight * cnt
fun amount() = product.price * cnt
}
// mian方法处理:
fun main() {
val iPhone = Product("iPhone", Price(3.5), Weight(0.5))
val xiaomi = Product("xiaomi", Price(1.5), Weight(0.2))
val orderLines = listOf(OrderLine(iPhone, 2), OrderLine(xiaomi, 3))
val weight: Weight = orderLines.fold(Weight(0.0)) { accelerate, b ->
accelerate + b.weight()
}
val amount: Price = orderLines.fold(Price(0.0)) { accelaerate, b ->
accelaerate + b.amount()
}
print("weight:$weight, amount:$amount")
}
如果编译器没有报错,就说明我们使用了正确的类型,这个时候,我们已经把类型检查做的很好了!
当然如果想要深化,那确实还有可以深化的地方,比如 不可能有 0 值的重量或者价格的商品,我们不希望别人在构造我们的对象时,声明了 Price(0.0) 、 Weight(0.0) 这样的代码,如果遇到了,我们希望抛出异常。 而如果他们想使用这个对象,来放在 fold 或者 reduce 中来做累加, 我们可以提供一个单位函数给他们使用(单位函数就是类似于加法中的0,乘法中的1这样的中立元素)。
为了达到目的, 我们可以使用私有构造函数和工厂函数,对于 Price, 可以修改为如下代码:
data class Price private constructor(private val value: Double) {
override fun toString(): String = value.toString()
operator fun plus(price: Price) = Price(this.value + price.value)
operator fun times(num: Int) = Price(this.value * num)
companion object {
// 这里是我们定义的单位元素,因为是从
val identity = Price(0.0)
operator fun invoke(value: Double): Price {
if (value > 0) {
Price(value)
} else
// 如果别人定义了 Price(0.0) 则在运行时抛出异常
throw IllegalArgumentException("Price must be more than 0")
}
}
}
构造函数现在是私有的,伴生对象 invoke
函数现在被声明为 operator 并包含验证代码, 相当于被重载了 .()
运算符
因此,可以完全像构造函数一样使用工厂函数,最终处理订单的代码如下所示:
fun main() {
val iPhone = Product("iPhone", Price(3.5), Weight(0.5))
val xiaomi = Product("xiaomi", Price(1.5), Weight(0.2))
val orderLines = listOf(OrderLine(iPhone, 2), OrderLine(xiaomi, 3))
val weight: Weight = orderLines.fold(Weight.identity) { accelerate, b ->
accelerate + b.weight()
}
val amount: Price = orderLines.fold(Price.identity) { accelerate, b ->
accelerate + b.amount()
}
print("weight:$weight, amount:$amount")
}