2

我们进行了多年的Android开发,但是面对越来越复杂的业务逻辑和越来越庞大的代码,传统命令式的编程方式已经渐渐无法解决我们的问题了。今天开始我们将探索一种非常强大的编程范式:函数式编程。

1. 传统编程范式的挑战

1.1 过程式编程的难题

大家日常开发中一定遇到过这些问题:

1.1.1 返回值不确定

// 全局计数器变量
var counter = 0

// 返回值依赖于外部状态,每次调用结果不同
fun getNextId(): Int {
    return counter++
}

在Android开发中,这种情况经常出现在多个Fragment或Activity共享状态时。

1.1.2 属性值不确定(变量)

// 用户信息存储在全局变量中
var currentUser: User? = null

// 在不同生命周期阶段访问可能得到不同结果
fun displayUserInfo() {
    // currentUser可能在任何时候被其他组件修改
    currentUser?.let { 
        binding.userName.text = it.name 
    }
}

1.1.3 回调地狱

这个在Android开发中实在太常见了:

// 经典的Android回调地狱
userRepository.getUser { user ->
    postRepository.getPostsByUser(user.id) { posts ->
        imageLoader.loadProfileImage(user.avatarUrl) { avatar ->
            analyticsService.trackUserView(user.id) {
                // 此时逻辑已经嵌套四层
                updateUI(user, posts, avatar)
            }
        }
    }
}

1.1.4 线程不安全

// 多线程环境下共享变量的问题
class SharedCounter {
    var count = 0
    
    fun increment() {
        // 在多线程环境下,这个操作不是原子的
        count++
    }
}

1.2 面向对象编程的难题

1.2.1 类的单继承

Android开发中非常常见的问题:

// Android中的经典问题
abstract class BaseActivity : AppCompatActivity() {
    // 基础功能
}

// 如果我们想同时继承另一个基类,就无法做到
class MyActivity : BaseActivity(), SomeInterface {
    // 无法再继承其他基类
}

1.2.2 组合性差

// 为了适应不同场景,方法重载成灾
class UserManager {
    fun fetchUser(id: Int) { /* ... */ }
    fun fetchUser(email: String) { /* ... */ }
    fun fetchUser(id: Int, withPosts: Boolean) { /* ... */ }
    fun fetchUser(id: Int, withPosts: Boolean, withComments: Boolean) { /* ... */ }
    // 随着需求增加,重载方法数量呈指数级增长
}

1.2.3 模板代码众多

// 标准的Android RecyclerView Adapter模板代码
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
    private val users = mutableListOf<User>()
    
    fun updateUsers(newUsers: List<User>) {
        users.clear()
        users.addAll(newUsers)
        notifyDataSetChanged()
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 样板代码...
    }
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 样板代码...
    }
    
    override fun getItemCount() = users.size
    
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        // 样板代码...
    }
}

2. 函数式编程的解决方案

2.1 不可变性(Immutability)

2.1.1 变量的问题(值的不确定性)

如果经历的项目足够多,就能发现,"变量"往往是很多Bug的根本来源。普通的变量改变的时间不确定、变更的值不确定,使得防御性检查在某些情况下也会失效。

来看一个Android中的实际例子:

// 传统方式:使用可变列表
class PostsViewModel : ViewModel() {
    val posts = mutableListOf<Post>()
    
    fun loadPosts() {
        // 在某个线程中加载数据
        repository.getPosts { newPosts ->
            posts.clear()
            posts.addAll(newPosts)
            // 此时如果有其他线程正在读取posts...
        }
    }
}

2.1.2 使用val代替var

Kotlin中的val关键字帮助我们定义不可变引用:

// 函数式方式:使用不可变引用
class PostsViewModel : ViewModel() {
    // 使用LiveData或Flow来表示变化的状态
    private val _posts = MutableLiveData<List<Post>>(emptyList())
    val posts: LiveData<List<Post>> = _posts
    
    fun loadPosts() {
        repository.getPosts { newPosts ->
            // 整体替换,不是修改
            _posts.value = newPosts
        }
    }
}

这里的关键区别是:我们不是修改列表本身,而是创建一个新的列表替换掉旧的。

2.1.3 data class(对象值变化的判定)

Kotlin的data class为不可变编程提供了极好的支持:

// 不可变的数据模型
data class User(
    val id: Int,
    val name: String,
    val email: String
)

// 需要修改时,创建新对象而非修改原对象
val user = User(1, "John", "john@example.com")
val updatedUser = user.copy(name = "John Doe")

2.1.4 不可变容器

RecyclerView中的一个常见问题:

// 传统方式:使用可变列表容易出现异步问题
class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
    private val posts = mutableListOf<Post>()
    
    fun updatePosts(newPosts: List<Post>) {
        posts.clear()
        posts.addAll(newPosts)
        notifyDataSetChanged()
    }
    
    override fun getItemCount() = posts.size
    
    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        // 如果在getItemCount()后但在这个方法前,
        // 其他线程修改了posts,可能导致IndexOutOfBoundsException
        val post = posts[position]
        holder.bind(post)
    }
}

函数式解决方案:

// 函数式方式:使用不可变列表
class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
    // 使用不可变列表
    private var posts: List<Post> = emptyList()
    
    fun updatePosts(newPosts: List<Post>) {
        // DiffUtil计算差异,高效更新
        val diffResult = DiffUtil.calculateDiff(PostDiffCallback(posts, newPosts))
        // 整体替换而非修改
        posts = newPosts
        diffResult.dispatchUpdatesTo(this)
    }
    
    override fun getItemCount() = posts.size
    
    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        // posts是一个不变的引用,安全地访问
        val post = posts[position]
        holder.bind(post)
    }
}

2.1.5 性能问题?

对于一些开发者而言,"每次创建新对象"会引起性能担忧。但事实上:

  1. 结构共享:不可变容器在修改后创建的新对象并不会将旧容器中所有数据全部深度拷贝。Kotlin标准库中的persistent collections就采用了这种优化。
  2. 优化空间:由于"不可变"这个基本共识,反而可以做出很多优化。
  3. VM优化:现代JVM对于短生命周期的小对象有专门的优化(年轻代GC)。
  4. 权衡取舍:在Android应用开发中,我们更多面对的是善变的需求和复杂的业务逻辑,而不是极端性能场景。不可变性带来的代码健壮性远超过其微小的性能损失。

来看个实际的例子 - 使用不可变列表的结构共享:

// Haskell中的链表实现方式在概念上类似这样
sealed class ImmutableList<out T> {
    object Nil : ImmutableList<Nothing>()
    data class Cons<T>(val head: T, val tail: ImmutableList<T>) : ImmutableList<T>()
}

// 添加新元素只需创建一个新的头节点,复用原有的尾部
fun <T> ImmutableList<T>.prepend(item: T): ImmutableList<T> = 
    ImmutableList.Cons(item, this)

在Haskell中,这种链表实现允许高效地共享结构:

-- 定义一个列表
originalList = [3,4,5]

-- 添加一个元素,但原始列表不变
newList = 2 : originalList  -- newList现在是[2,3,4,5]

-- originalList仍然是[3,4,5]
-- 两个列表共享了[3,4,5]部分的内存

2.2 副作用与纯函数

2.2.1 什么是纯函数

纯函数是函数式编程的核心概念:对于相同的输入,总是产生相同的输出,并且没有副作用

// 不纯的函数 - 依赖外部状态
var discount = 0.0

fun calculatePrice(basePrice: Double): Double {
    return basePrice * (1 - discount)  // 依赖外部变量
}

// 纯函数 - 只依赖输入参数
fun calculatePrice(basePrice: Double, discount: Double): Double {
    return basePrice * (1 - discount)  // 只依赖参数
}

在Android开发中的一个实际例子:

// 不纯的方式
class PriceCalculator {
    // 依赖于外部状态
    var taxRate = 0.1
    
    fun calculateTotalPrice(price: Double): Double {
        // 使用外部状态,结果取决于taxRate的当前值
        return price * (1 + taxRate)
    }
}

// 纯函数方式
class PriceCalculator {
    // 将所有依赖作为参数传入
    fun calculateTotalPrice(price: Double, taxRate: Double): Double {
        // 只依赖于输入参数,结果可预测
        return price * (1 + taxRate)
    }
}

2.2.2 无副作用

副作用是指函数在返回值之外对程序状态的任何修改。包括:

  • 修改全局变量或对象
  • 写入文件或数据库
  • 网络请求
  • 显示在屏幕上
  • 抛出异常

Android中的副作用例子:

// 含有副作用的函数
fun saveUser(user: User): Boolean {
    try {
        database.insert(user)  // 副作用:修改数据库
        preferences.edit().putString("last_user", user.name).apply()  // 副作用:修改SharedPreferences
        analytics.trackEvent("user_saved")  // 副作用:发送分析事件
        return true
    } catch (e: Exception) {
        Log.e("UserManager", "Failed to save user", e)  // 副作用:日志输出
        return false
    }
}

2.2.3 参数唯一决定返回值(记忆模式)

纯函数的一个重要特性是可以缓存其结果,这就是"记忆模式":

// 记忆化的斐波那契函数
class MemoizedFibonacci {
    private val cache = mutableMapOf<Int, Long>()
    
    fun fibonacci(n: Int): Long {
        // 如果结果已经计算过,直接返回缓存的值
        return cache.getOrPut(n) {
            when (n) {
                0 -> 0
                1 -> 1
                else -> fibonacci(n - 1) + fibonacci(n - 2)
            }
        }
    }
}

Android UI中的应用:

// 在Jetpack Compose中的memoization
@Composable
fun ExpensiveUI(data: ComplexData) {
    // remember函数基于传入的键值缓存结果
    // 只有当data变化时才会重新计算
    val processedResult = remember(data) {
        expensiveComputation(data)
    }
    
    Text(text = "Result: $processedResult")
}

2.2.4 必须有返回值

在函数式编程中,每个函数都应该有返回值。即使是控制结构(如ifwhen)在Kotlin中也被设计为表达式:

// if作为表达式有返回值
val max = if (a > b) a else b

// when作为表达式有返回值
val stringRepresentation = when (color) {
    Color.RED -> "Red"
    Color.GREEN -> "Green"
    Color.BLUE -> "Blue"
    else -> "Unknown"
}

在Android开发中的应用:

// UI状态转换
val uiState = when {
    isLoading -> UiState.Loading
    error != null -> UiState.Error(error)
    data != null -> UiState.Success(data)
    else -> UiState.Empty
}

// 基于UI状态渲染不同内容
binding.contentView.isVisible = uiState is UiState.Success
binding.errorView.isVisible = uiState is UiState.Error
binding.loadingView.isVisible = uiState is UiState.Loading
binding.emptyView.isVisible = uiState is UiState.Empty

2.3 纯函数的好处

2.3.1 测试简单

纯函数非常容易测试,因为输入确定,输出也确定:

// 很容易测试的纯函数
fun calculateDiscount(price: Double, discountRate: Double): Double {
    return price * discountRate
}

// 单元测试
@Test
fun `calculateDiscount correctly applies discount rate`() {
    // 给定固定输入,输出总是确定的
    assertEquals(20.0, calculateDiscount(100.0, 0.2), 0.001)
    assertEquals(50.0, calculateDiscount(100.0, 0.5), 0.001)
    assertEquals(0.0, calculateDiscount(100.0, 0.0), 0.001)
}

2.3.2 行为可预测

函数签名可以清晰表达函数的行为:

// 函数签名清晰表明行为和依赖
fun combineUserData(
    profile: UserProfile, 
    settings: UserSettings
): UserData {
    // 从签名就可以看出,这个函数只依赖传入的两个参数
    return UserData(
        id = profile.id,
        name = profile.name,
        email = profile.email,
        preferences = settings.preferences
    )
}

2.3.3 方便优化

编译器和运行时可以对纯函数进行各种优化,如并行执行、缓存结果等。

2.4 函数第一性

2.4.1 一切皆为函数

在函数式编程中,函数是"一等公民",可以像值一样被传递和操作:

// 函数作为参数传递
fun processItems(items: List<Int>, processor: (Int) -> Int): List<Int> {
    return items.map(processor)
}

// 使用不同的处理函数
val doubled = processItems(listOf(1, 2, 3)) { it * 2 }  // [2, 4, 6]
val squared = processItems(listOf(1, 2, 3)) { it * it } // [1, 4, 9]

在Android中的实际应用:

// RecyclerView中的列表项点击处理
interface ItemClickListener {
    fun onItemClick(item: ListItem)
}

// 函数式方式更简洁
class ItemsAdapter(
    private val items: List<ListItem>,
    private val onItemClick: (ListItem) -> Unit  // 函数类型作为参数
) : RecyclerView.Adapter<ItemViewHolder>() {
    // ...
    
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = items[position]
        holder.bind(item)
        holder.itemView.setOnClickListener { onItemClick(item) }
    }
}

// 使用时
val adapter = ItemsAdapter(items) { clickedItem ->
    // 处理点击事件
    navigateToDetail(clickedItem.id)
}

2.4.2 惰性计算

惰性计算是只在需要结果时才执行计算的技术。Kotlin通过Sequence提供了惰性计算的支持:

// 热切计算 - 立即执行所有操作
val result = listOf(1, 2, 3, 4, 5)
    .map { println("Map: $it"); it * 2 }
    .filter { println("Filter: $it"); it > 5 }
    .first()
// 输出所有map操作,然后所有filter操作

// 惰性计算 - 按需执行
val result = sequenceOf(1, 2, 3, 4, 5)
    .map { println("Map: $it"); it * 2 }
    .filter { println("Filter: $it"); it > 5 }
    .first()
// 只输出需要的操作:Map: 1, Filter: 2, Map: 2, Filter: 4, Map: 3, Filter: 6

一个完整的斐波那契数列生成器例子:

// 生成无限的斐波那契数列
fun fibonacciSequence(): Sequence<Long> = sequence {
    var a = 0L
    var b = 1L
    
    // 返回第一个数字 0
    yield(a)
    
    // 返回第二个数字 1
    yield(b)
    
    // 生成后续的斐波那契数
    while (true) {
        val next = a + b
        yield(next)
        
        // 更新值以计算下一个数字
        a = b
        b = next
    }
}

// 使用时只计算需要的部分
val first10Fibonacci = fibonacciSequence().take(10).toList()
println(first10Fibonacci)  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

在Android中的应用:

// 处理大型数据集
fun processLargeLogFile(context: Context, logUri: Uri): Sequence<LogEntry> {
    return sequence {
        context.contentResolver.openInputStream(logUri)?.use { stream ->
            BufferedReader(InputStreamReader(stream)).use { reader ->
                // 逐行读取并处理日志文件
                var line = reader.readLine()
                while (line != null) {
                    // 解析日志行
                    val entry = parseLine(line)
                    
                    // 只在需要时才解析并产生下一个结果
                    yield(entry)
                    
                    // 读取下一行
                    line = reader.readLine()
                }
            }
        }
    }
}

// 使用时可以高效处理
processLargeLogFile(context, logUri)
    .filter { it.level == LogLevel.ERROR }
    .take(10)
    .forEach { showErrorLog(it) }

2.5 异常处理

函数式编程中,异常也被看作值的一部分,而不是特殊的控制流:

// 传统的异常处理
fun parseJson(json: String): User {
    try {
        return jsonParser.parse(json)
    } catch (e: Exception) {
        Log.e("Parser", "Failed to parse", e)
        throw e  // 或返回默认值
    }
}

// 函数式的错误处理 - 使用Result
fun parseJson(json: String): Result<User> {
    return runCatching {
        jsonParser.parse(json)
    }
}

// 使用
parseJson(jsonString)
    .onSuccess { user ->
        // 处理成功情况
        displayUser(user)
    }
    .onFailure { error ->
        // 处理错误情况
        showError(error.message)
    }

另一个例子,使用Either类型(在Arrow库中可用):

// 使用Either类型处理错误
sealed class ParseError {
    data class InvalidFormat(val message: String) : ParseError()
    data class MissingField(val field: String) : ParseError()
    data class NetworkError(val code: Int) : ParseError()
}

// 返回Either<ParseError, User>类型
fun parseUser(json: String): Either<ParseError, User> {
    // 如果解析成功,返回Right(user)
    // 如果有错误,返回Left(error)
}

// 使用
when (val result = parseUser(jsonString)) {
    is Either.Left -> {
        // 处理错误
        when (val error = result.value) {
            is ParseError.InvalidFormat -> showFormatError(error.message)
            is ParseError.MissingField -> promptForField(error.field)
            is ParseError.NetworkError -> retryWithBackoff(error.code)
        }
    }
    is Either.Right -> {
        // 处理成功情况
        val user = result.value
        displayUser(user)
    }
}

2.6 循环、递归与尾递归

递归是函数式编程中替代循环的主要方式,因为它不需要使用变量:

// 使用变量的迭代方式计算阶乘
fun factorial(n: Int): Long {
    var result = 1L
    for (i in 1..n) {
        result *= i
    }
    return result
}

// 使用递归计算阶乘
fun factorialRecursive(n: Int): Long {
    return if (n <= 1) 1 else n * factorialRecursive(n - 1)
}

但递归可能导致栈溢出,Kotlin提供了尾递归优化:

// 使用尾递归优化的阶乘计算
tailrec fun factorialTail(n: Int, acc: Long = 1): Long {
    return if (n <= 1) acc else factorialTail(n - 1, n * acc)
}

我们可以用尾递归重写斐波那契函数:

// 尾递归版本的斐波那契函数
tailrec fun fibonacciNext(a: Long, b: Long, ordinal: Int): Long =
    if (ordinal == 1)
        a + b
    else
        fibonacciNext(b, a + b, ordinal - 1)

fun fibonacci(ordinal: Int): Long =
    when (ordinal) {
        1 -> 0L
        2 -> 1L
        else -> fibonacciNext(0L, 1L, ordinal - 2)
    }

Y组合子简介

Y组合子是函数式编程中一个高级概念,它允许在没有显式递归能力的纯函数式语言中实现递归:

// Y组合子类型定义
typealias RecursiveFunc<T, R> = ((T) -> R) -> (T) -> R

// Y组合子实现
fun <T, R> Y(f: RecursiveFunc<T, R>): (T) -> R {
    val g: ((T) -> R, T) -> R = { func, x -> f { y -> func(y) }(x) }
    return { x -> 
        g(run {
            fun h(n: T): R = g(::h, n)
            ::h
        }, x) 
    }
}

// 使用Y组合子实现斐波那契
val fibonacci = Y<Int, Long> { f ->
    { n ->
        when (n) {
            0 -> 0L
            1 -> 1L
            else -> f(n - 1) + f(n - 2)
        }
    }
}

// 使用
println(fibonacci(10))  // 55

这个概念在实际Android开发中用得比较少,但理解它有助于加深对函数式编程本质的理解。


Yumenokanata
33 声望158 粉丝

Haskell中毒极深的Android开发工程师