Kotlin 用函数编程

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin 用函数编程相关的知识,希望对你有一定的参考价值。

文章目录

目标主要是为了弄清楚:
● 使用函数式风格来编程的意义是什么?
● 为什么我要将函数作为参数传递?我定义一个接口,让他们来调用不就好了?我为什么要把函数作为一个值

1. 函数的概念

1.1 数学中的函数

函数是我们从小到大就在数学中接触的概念,在数学课本中函数的定义是这样的:

给定一个数集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

1.1.1 偏函数

下面有两个条件

  • A. 必须是定义域对所有元素进行定义
  • B. 定义域中的元素不能和值域中的多个元素对应

满足 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 时,要么是正确结果,要么是符合预期的错误结果。

偏函数转化成全函数是安全编程的一个重要部分!!!!!, 而上面转化成全函数的两种做法也正是我们常用的处理偏函数的常用方式,即:

  1. 对定义域进行指定、定义
  2. 向值域添加错误的元素

1.1.2 多参数的函数

在编程中, 我们的函数经常看起来不止有一个入参,例如 f(x, y) = x * y,而函数的定义是 一个源集 到 一个目标集 的关系,那这还是函数吗?

答案是肯定的,我们引入了 元组(tuple) 这个特例概念, 即 (x, y) 甚至 (x, y, z) 都是一个元组,这个元组的定义域就是源集。所以是没有 多参数函数 这种概念的。

1.1.3 柯里化函数

柯里化函数是对上面所讲的 元组函数 的变形。
假定 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) 的柯里化形式, 数学中称这种函数为柯里化函数

1.1.4 偏应用函数

这个概念很好理解,它是在柯里化函数上进行深化的。

例如函数: f(x, y) = x + y 那么它等价于 f(x)(y)
f(x) 是什么呢? 它代表的是自变量 x 对应的映射函数 , 当 x = 1时,f(1) 的结果 是一个函数,这个函数:输入是 y, 结果是 y 加上这个1。 那么我们称 f(x)f(x, y) 对x的偏应用函数 ,偏应用函数会对自变量计算产生很大的影响,这个我们会在后面讲到。

1.2 Kotlin中的函数

在 Kotlin 中:

  1. 函数是数据
    函数是有类型的,可以被传递,也可以被返回,也可以被放到集合中
  2. 数据也可以是函数
    数据可以看成是 源集任意,目标集只有一个 , 也可以称为 常函数
    例如 val x = 5 , 可以看成是一个 f(x)=5 的函数,就是自变量无关的一种特殊函数

1.2.1 纯函数

1.2.1.1 定义

前面讲过数学中的全函数, 在 Kotlin 中,程序员创造了一个与之相似的概念,叫 “纯函数”, 这是因为虽然编程语言定义了 fun 关键字来声明函数,但是很多时候,程序员所写的函数很少能称为真正的函数,所以提出这个概念,愿景是希望Kotlin开发者能够多写真正的函数。

函数要成为纯函数的条件如下:

  • 不能改变函数外界的任何事物
  • 内部的改变对外部不可见
  • 不能改变入参
  • 对于相同的参数, 无论何时执行,始终只返回同一个值
  • 函数不能抛出异常或错误(不能出现crash)
  • 始终返回一个值

1.2.1.2 例子

请看下面代码的 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 对象

1.2.2 值函数

Kotlin 是允许将函数写成数据的
例如 下面的函数:

fun add(a: Int, b: Int): Int = a + b

等价于:

val sum :(Int, Int) -> Int = a, b -> a + b

这里用了 lambda 表达式,这里就不再赘述,之前学习过:Lambda学习

在 Kotlin 中,函数有两种定义形式,一种是通过 fun 关键字定义,一种是使用 值 来定义,他们的区别是什么呢?为什么不像 Java 那样只用一种方法来定义呢?

  1. 通过 fun
    Kotlin 是会优化 fun 关键字声明的函数,更优效率,并且更加美观
  2. 通过值函数
    可以作为数据传递, 或者作为变量存储到 list、map中
    (当然 fun 声明的函数也可以做为对象传递, 使用 ::funName 的形式 )

1.2.3 复合函数

下面我们将两个函数进行复合, 我们不仅要学习拆分函数的,也需要学会聚合函数,例如下面的两个函数:

// 复合下面两个函数,做到先乘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 函数。

2. 高级函数的特征

上面一章学了函数的概念, 但是还没有解答一个最基本的问题,即为什么要将函数作为数据,进行使用或者传递,为什么不只是用 fun 版本? 下面需要来考虑处理多参数的函数。

2.1 多参数函数

没有多个参数的函数, 只有多个参数组成的元组的函数, 就是元组的参数可以是任意多个, 它本身可以是 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)

2.2 高阶函数的定义

在上一章中,我们为了达到函数复合 ,编写了一个 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)

2.2.1 compose 的多态高阶版本

上面的 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))

2.3 使用匿名函数

我们可以使用匿名函数来省略中间函数的定义,例如:

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)

一般来说,匿名函数还命名函数,选择是任意的, 通常下,如果一个函数只使用一次,可以把这个函数弄成匿名函数

2.4 闭包

看下下面代码:

val taxRate = 0.09
fun addTax(price: Double) = price + price * taxRate

上面的 addTaxprice 来说不是一个纯函数,因为函数依赖了参数以外的属性 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
  

2.5 应用偏函数和自动柯里化

上面写了闭包类型和柯里化类型,虽然对于同样的入参,他们的结果是一样的,但是他们的语义是不一样的
闭包是一股脑的将参数塞入,而柯里化则是层层递进。

上面的柯里化版本,其实就等价于下面的类:

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)

可以看到,其实柯里化函数和偏应用函数是密切相关的,可以做到一个参数接一个参数将一个元组给替换为可偏应用的函数。这就是其和元组参数的区别。

2.5.1 例1

试着写一个函数, 双参的柯里化函数,偏应用其第一个参数,推演如下:

假设双参为 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)

2.5.2 例2

试着写一个函数, 也是双参的柯里化函数,偏应用其第二个参数,推演如下:

同样的使用上面的 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)

2.5.3 例3

写一个函数来将柯里化 (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)
    

2.6 切换偏应用函数的参数

假如一个函数有两个参数,但有时候我们不想直接得到结果,(例如其中一个参数还不清楚值),我们只想通过一个参数来获得一个偏应用函数,例如下面的:

val addTax: (Double) -> (Double) -> Double =  x->
  y ->
    y + y/100 * x
  

开发者可能想先计算税,然后获得一个参数的新函数,然后可以将该函数应用于任何价格:

val add9percentTax: (Double) -> Double = addTax以上是关于Kotlin 用函数编程的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin编程语言

Kotlin总结

函数式编程———内联函数

kotlin方法传入lambda表达式参数并调用invoke什么意思

《Kotlin核心编程》笔记:函数和Lambda表达式

kotlin学习笔记之闭包