Kotlin 函数式编程思想

作为一个使用了很多年 Java 的开发者☕️,面向对象思想早已深入人心,但 Kotlin 语言本身是天然支持函数式编程的,如果你对此没什么感觉,那么请多用 Compose 。

但是 Kotlin 的函数式编程思想并不是仅仅可以在 Compose 上发挥作用,我们在任何地方都可以使用函数式编程来解决问题。相对于面向对象,函数式在很多时候都具有独特的优势,我们可以灵活的在合适的地方用合适的方式来编写代码。

倒不是说面向对象不好,面向对象是用来解决特定问题的,它可以很好的对事物做抽象和建模,但我们也不能陷入面向对象的条条框框之中。

在 Android 开发中的函数式也有一些特有的问题需要解决,本文主要就是来介绍 Kotlin 函数式编程以及 Android 开发中的函数式编程。

函数式最早可以追溯到一百多年前,数学家希尔伯特提出了著名的希尔伯特计划:希望建立一种严格的数学基础,保证所有数学问题都能通过形式化方法解决。其中的关键问题之一就是可判定性问题(Entscheidungsproblem)。 这个问题在 1936 年被两个人同时解决,其中一个人是图灵,另一个人就是阿隆佐·邱奇。

邱奇在 1930 年代开发的 λ 演算,是建造自函数应用的一种计算形式系统。在 1937 年,艾伦·图灵证明了 λ 演算和图灵机是等价的计算模型,展示了 λ 演算是图灵完备性的。λ 演算形成了所有函数式编程语言的基础。

Kotlin 并不像 Haskell 一样是纯函数式编程语言,而是混合式的,其语法层面上是都支持的,我们可以灵活的切换编程方式,扬长避短。

函数式编程的概念

函数式编程是一种编程范式,将函数作为一等公民(first-class citizen),避免使用程序的状态和定义对象及变量,函数可以作为参数也可以作为返回值,程序的运行则是利用若干简单的函数计算逐级推导至复杂运算,最终完成计算过程,返回计算结果。

顶级函数

在 Kotlin 中我们会有个顶级函数的概念,也就是在一个 kt 文件中直接声明函数,而不是将函数声明在类的内部。这意味着我们可以直接把函数当作一段逻辑计算的入口,不需要通过类创建对象这样的方式,这也是函数式中的重要概念,也可以说是一个支持函数式编程语言的必备特性。

依赖注入怎么办?

如果都使用函数,那么就要解决依赖注入的问题,不过我知道的有些依赖注入框架是支持函数注入的,你可以直接在函数中加上参数,然后依赖注入框架会直接帮你注入。

kotlin 复制代码
typealias myFunction = () -> Unit

@Inject
fun myFunction(dep: Dep) {
}

当然我们的目的并不是打造一个纯函数式编程程序,既然是混合,那么类是完全可以接受的,我们仍然可以在类中使用依赖注入。

不过 Kotlin 似乎也考虑到了这一点,所以提供了 invoke 运算符重载,用过 UseCase 的人都知道这是怎么回事。

kotlin 复制代码
class UpdateActivityPubUserListUseCase @Inject constructor(
    private val contentRepo: FreadContentRepo,
) {
    suspend operator fun invoke(
        content: ActivityPubContent,
        allUserCreatedList: List<ListTimeline>
    ) {
        // do something
    }
}

看起来就是个普通的类,可以通过依赖注入在构造器内注入参数,区别在于使用了 operator 修饰符,重点是使用的时候可以像一个函数一样使用。

jsx 复制代码
class XXXViewModel @Inject constructor(
    private val updateActivityPubUserList: UpdateActivityPubUserListUseCase,   
): ViewModel(){
    
    init {
        viewModelScope.launch { 
		        // It's actually calling the invoke method.
            updateActivityPubUserList()
        }
    }
}

函数类型

函数作为一等公民的意思是函数可以作为一种变量的类型而存在,它并不会因为是函数而有什么特别之处,函数可以作为变量存储、作为参数传递、作为返回值。

jsx 复制代码
val add: (Int, Int) -> Int = { x, y -> x + y }
println(add(2, 3)) // 5

函数也可以接收其他函数作为参数。

jsx 复制代码
fun operate(x: Int, y: Int, f: (Int, Int) -> Int): Int = f(x, y)
println(operate(2, 3, { a, b -> a * b })) // 6

总之在 Kotlin 中使用函数非常灵活,虽然 Kotlin for JVM 也会把函数编译成类对象,但这毕竟是 JVM 的限制,而 Kotlin 本身是跨平台的,只要在源码层面可以将函数视为一等公民那就没什么问题了。

纯函数与幂等函数

幂等函数和纯函数都是函数式编程中的重要概念。幂等函数是指同样的输入多次执行结果保持不变。纯函数是指输入相同的参数输出的结果必然保持一致,并且没有副作用。

一个干净整洁的函数对于函数式编程来说非常重要,因为在函数式中函数的定义是一段特定的逻辑计算,它只接受函数的入参作为计算的依赖项,然后返回计算结果,通过连续地函数调用来完成整个复杂任务。除此之外不应该有任何其他的操作。既不应该依赖外部的状态,也不应该修改外部的状态。

副作用(Side Effect)

写过 Compose 或者 React 的开发者应该对副作用的概念很清楚,在声明式 UI 框架中,UI 部分一般都是通过幂等函数来实现,但实际上我们总有一些 UI 之外的事情要做,或者我们总希望能获取到某些 UI 的信息或者时机,但是作为幂等函数我们是不能修改外部变量的,因此提供了专门的 API 来管理副作用。

也就是说,副作用可以理解为幂等函数运行过程中的一些额外的影响,为了保持幂等性,我们需要通过副作用 API 将本次运行函数的影响暴露出来。

kotlin 复制代码
@Composable
fun UserDetailPage(viewModel: UserDeailViewModel){
    LaunchedEffect(Unit){
        viewModel.onPageResume()
    }
    Scaffold { 
        // ...
    }
}

不过,严格来说,Composable 其实并不能算是纯函数或者幂等函数,他们只是在设计上尽可能按照这个方向来设计,思想上保持一定程度的一致性。因为,首先将 UI 绘制到屏幕上本身就是一种副作用,所以 composable 的目的本来就是为了产生副作用。其次,composable 并没有返回值,不存在相同的输入就有相同的输出,可以说绘制 UI 就是 composable 的输出。只不过我们先了解了函数式编程后再来理解 composable 就容易多了。

不可变性(Immutability)

函数式编程中的 不可变性 (Immutability) 指的是:一旦数据(属性、对象、集合)被创建,就不能被修改。如果数据发生变化那么应该创建一个新的数据,而不是在原值上改动。这样保证了函数调用之间不会"互相污染",也避免了一些隐藏的副作用。

这样我们很自然的就想到了 Kotlin 中的 valdata class ,通过 val 声明的属性是不可变的,一般来说 data clas 也是不可变的,Kotlin 虽然没有严格的要求,但是强烈推荐 data class 中的所有属性都必须使用 val 声明,否则会出现一些奇怪的问题。

并且 data class 提供了 copy 方法用于复制一个对象副本,在复制的时候可以修改其中的某些属性,这也符合函数式编程中的不可变性。

此外 Kotlin 中的集合默认也都是不可变集合,像 *arrayOf*() , *listOf*() , *mapOf*() 这种方式创建的集合都是不可变集合,考虑到方便的创建不可变集合,Kotlin 还贴心的提供了 buildList{ } 这样的函数防止我们为了省事创建了可变集合。

所以我现在在开发过程中已经很少使用变量了,如果看到某个地方声明了一堆 var 真的会引起生理不适🤮。这样的好处就是心智负担很低,不用担心这么多变量组合出千变万化的状态,整个程序逻辑变得异常清晰。

毕竟,多个变量共同作用组合出的状态可以说是指数级增加的,整个系统也会变的更加难以理解难以测试,实际上函数式思想就是将软件变成对数据的计算过程,如果合理的设计函数,那么可读性也会大大提高。

当然了,我们开发软件大部分使用的仍然是面向对象,函数式还是作为面向对象的补充,并且其中的一些思想完全可以借鉴,比如我们虽然还是要创建一个类,但是其中的属性可以尽量不可变,也不必排斥顶级函数,这不是什么语法糖🍬,这是函数式编程的体现。不用坚持某些教条主义,在实践中灵活的改变编程范式或许能让代码更优雅更简洁。

相关推荐
wx_lidysun5 小时前
Nextjs学习笔记
前端·react·next
无羡仙7 小时前
从零构建 Vue 弹窗组件
前端·vue.js
xiaolizi5674898 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰100018 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜8 小时前
Python入门篇【文件处理】
android·java·python
源心锁8 小时前
👋 手搓 gzip 实现的文件分块压缩上传
前端·javascript
源心锁9 小时前
丧心病狂!在浏览器全天候记录用户行为排障
前端·架构
GIS之路9 小时前
GDAL 实现投影转换
前端
烛阴9 小时前
从“无”到“有”:手动实现一个 3D 渲染循环全过程
前端·webgl·three.js
BD_Marathon9 小时前
SpringBoot——辅助功能之切换web服务器
服务器·前端·spring boot