Kotlin实战之Fuel的高阶函数

Fuel 是一个用 Kotlin 写的网络库,与 OkHttp 相比较,它的代码结构比较简单,但是它的巧妙之处在于充分利用了 Kotlin 的语言特性,所以代码看上去干净利落。

OkHttp 使用了一个 interceptor chain 来实现拦截器的串联调用,由于 Java 语言( JDK ≤ 7)本身的局限性,所以实现代码比较臃肿,可读性也不友好。当然,RxJava 再加上 retrolambda 这种 backport 的出现,一定程度上了缓解了这种尴尬,但是 Kotlin 天生具备的声明式写法又使得 Java 逊色了很多。

我们知道,拦截器本质上是一个责任链模式(chain of responsibility)的实现,我们通过具体代码来学习一下 Kotlin 究竟是如何利用高阶函数实现了拦截器功能。

首先定义一个 MutableList 用于存储拦截器实例:

val requestInterceptors: 
  MutableList<((Request) -> Request) -> ((Request) -> Request)> 
   = mutableListOf()

注意,Kotlin 的类型系统明确区分了 mutable 和 immutable,默认的 List 类型是 immutable。

requestInterceptors 的元素类型是一个高阶函数

((Request) -> Request) -> ((Request) -> Request)

作为元素类型的高阶函数,其参数也是一个高阶函数 (Request) -> Request, 同时,返回值也是高阶函数 (Request) -> Request

然后,我们给 requestInterceptors 定义一个增加元素的方法:

fun addRequestInterceptor(
  interceptor: ((Request) -> Request) -> ((Request) -> Request)) {
    requestInterceptors += interceptor
}

addRequestInterceptor 的参数类型

(Request) -> Request) -> ((Request) -> Request)

requestInterceptors 的元素类型一致。

注意,这里又出现了一个 Kotlin 有而 Java 没有的语言特性:操作符重载。

我们没有调用 requestInterceptors.add(interceptor),而是用了一个 plusAssign 的操作符 +=(MutableCollections.kt 中定义的操作符重载):

/**
 * Adds the specified [element] to this mutable collection.
 */
@kotlin.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

那么,此时应该定义一个拦截器的函数实例了:

fun <T> loggingRequestInterceptor() =
        { next: (T) -> T ->
            { t: T ->
                println(t.toString())
                next(t)
            }
        }

loggingRequestInterceptor 是一个函数,它的返回值是一个 lambda 表达式(即高阶函数):

{ next: (T) -> T ->
    { t: T ->
        println(t.toString())
        next(t)
    }
}
  1. 这个 lambda 的参数next: (T) -> T(参数名是 next,参数类型是 (T) -> T),返回值是另一个 lambda 表达式:
{ t: T ->
    println(t.toString())
    next(t)
}
  1. 因为 lambda 本身是一个函数字面量(function literal),它的类型通过函数本身可以推到得出,如果我们用一个变量来引用这个 lambda 的话,变量的类型是 (T) -> T

由1、2两点可知,loggingRequestInterceptor() 的返回值是一个 lambda 表达式,它的参数是 (T) -> T,返回值也是 (T) -> T

这里的泛型函数略抽象,我们来看一个具体化的函数:

fun cUrlLoggingRequestInterceptor() =
        { next: (Request) -> Request ->
            { r: Request ->
                println(r.cUrlString())
                next(r)
            }
        }

同理,cUrlLoggingRequestInterceptor() 函数的参数为 (Request) -> Request、返回值为 (Request) -> Request

拦截器都定义好了,那么应该如何调用呢?Kotlin 一行代码搞定🤟::

requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }

foldRightList 的一个扩展函数,先来看声明:

/**
 * Accumulates value starting with [initial] value and applying [operation] from right to left to each element and current accumulator value.
 */
public inline fun <T, R> List<T>.foldRight(initial: R, operation: (T, acc: R) -> R): R {
    var accumulator = initial
    if (!isEmpty()) {
        val iterator = listIterator(size) // 让迭代器指向最后一个元素的末尾
        while (iterator.hasPrevious()) {
            accumulator = operation(iterator.previous(), accumulator)
        }
    }
    return accumulator
}

函数功能总结为一句话:从右往左,对列表中的每一个元素执行 operation 操作,每个操作的结果是下一次操作的入参,第一次 operation 的初始值是 initial

回头来看拦截器列表 requestInterceptors 如何执行了 foldRight

requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }

参数 inital: R 的实参是 { r: Request -> r },一个函数字面量,没有执行任何操作,接收 r 返回 r

参数 operation: (T, acc: R) -> R 可接收一个 lambda,所以它的实参 {f, acc -> f(acc)} 可以位于圆括号之外。f 的泛型是 T,具体类型是

((Request) -> Request) -> ((Request) -> Request)

acc 的类型通过 initial: R 的实参 { r: Request -> r } 可以推到得出——(Request) -> Request

OK,语法完全没毛病,再来看语义。

+---------------------+
| { r: Request -> r } | ---> 初始值,命名为 *fun0*
+---------------------+
           |
           |
          \|/    fun0 作为参数传递给 requestInterceptors 最右的 f(最后一个元素)
+----------------------------------|------------------------f---------------------|-+
| cUrlLoggingRequestInterceptor(): ((Request) -> Request) -> ((Request) -> Request) |
+----------------------------------|----------------------------------------------|-+
           |
           |                  f 返回结果:
           |                  +-----------------------------+
           |                  | { r: Request ->             |
           |                  |     println(r.cUrlString()) |
           |                  |     fun0(r)                 |
           |                  | }                           |
           |                  +-----------------------------+
           |                                    命名为 *fun1*
           |  
          \|/   fun1 作为参数,传递给倒数第二个 f
+----------------------------------|-----------------------f--------------------|-+
| loggingRequestInterceptor(): ((Request) -> Request) -> ((Request) -> Request)   |
+----------------------------------|--------------------------------------------|-+
           |
           |                  f 返回结果:
           |                  +-----------------------------+
           |                  | { r: Request ->             |
           |                  |     println(1.toString())   |
           |                  |     fun1(r)                 |
           |                  | }                           |
           |                  +-----------------------------+
           |                                    命名为 *fun2*
          \|/   将 fun2 解体:
+------------------------------+
| { r: Request ->              |
|     println(r.toString())    |
|     println(r.cUrlString())  | 类型为:(Request) -> request
|     r                        |
| }                            |
+------------------------------+

至此,一个简单的拦截器功能就实现了,代码竟然如此简洁,感动!

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,125评论 9 118
  • 原文链接:https://github.com/EasyKotlin 值就是函数,函数就是值。所有函数都消费函数,...
    JackChen1024阅读 5,936评论 1 17
  • 本文是在学习和使用kotlin时的一些总结与体会,一些代码示例来自于网络或Kotlin官方文档,持续更新... 对...
    竹尘居士阅读 3,261评论 0 8
  • 家居合肥,住在天鹅湖畔。每次逛湖,总有一种感恩的意识缠绕心头。远看霓虹灯七彩闪烁,近观湖水波光粼粼,我的心...
    湖畔渔夫阅读 242评论 0 0
  • 12月之后,连着好几天没见到太阳。天气阴沉,寒风肆虐。 从食堂回到宿舍的我,脸色苍白,双腿一刻也不敢停...
    逆风的漂流瓶阅读 217评论 0 0