异构列表(DslAdapter开发日志)

异构列表(DslAdapter开发日志)

函数范式, 或者说Haskell的终极追求是尽可能将错误"扼杀"在编译期, 使用了大量的手法和技术: 使用大量不可变扼杀异步的不可预计, 以及静态类型和高阶类型 java

说到静态类型你们应该都不会陌生, 它是程序正确性的强大保证, 这也是本人为何一直不太喜欢Python, js等动态类型语言的缘由git

静态类型: 编译时即知道每个变量的类型,所以,若存在类型错误编译是没法经过的。
动态类型: 编译时不知道每个变量的类型,所以,若存在类型错误会在运行时发生错误。

类型检查, 即在编译期经过对类型进行检查的方式过滤程序的错误, 这是咱们在使用Java和Kotlin等语言时经常使用的技术, 但这种技术是有限的, 它并不能通用于全部状况, 所以咱们经常反而会回到动态类型, 采用动态类型的方式处理某些问题 github

本文聚焦于常见的列表容器在某些状况下如何用静态类型的手法进行开发进行讨论编程

编译期错误检查

对于函数(方法)的输入错误有两种方式:segmentfault

  • 编译期检查, 好比List<String>中不能保存Integer类型的数据
  • 运行期检查, 好比对于列表的下标是否正确, 咱们能够在运行的时候检查

运行期检查是必需要运行到相应的代码时才会进行相应的检查(不管是实际程序仍是测试代码), 这是不安全而且效率低下的, 因此能在编译期检查的问题都尽可能在编译期排除掉 安全

编译期的检查中除了语法问题以外最重要的就是类型检查, 但这要求咱们提供足够的类型信息数据结构

DslAdapter实现中遇到的问题

DslAdapter是我的开发的一个针对Android RecyclerView的一个扩展库, 专一于静态类型和Dsl的手法, 但愿创造一个基于组合子的灵活易用同时又很是安全的Adapter 异步

在早期版本中已经实现了经过Dsl进行混合Adapter的建立:编程语言

val adapter = RendererAdapter.multipleBuild()
        .add(layout<Unit>(R.layout.list_header))
        .add(none<List<Option<ItemModel>>>(),
                optionRenderer(
                        noneItemRenderer = LayoutRenderer.dataBindingItem<Unit, ItemLayoutBinding>(
                                count = 5,
                                layout = R.layout.item_layout,
                                bindBinding = { ItemLayoutBinding.bind(it) },
                                binder = { bind, item, _ ->
                                    bind.content = "this is empty item"
                                },
                                recycleFun = { it.model = null; it.content = null; it.click = null }),
                        itemRenderer = LayoutRenderer.dataBindingItem<Option<ItemModel>, ItemLayoutBinding>(
                                count = 5,
                                layout = R.layout.item_layout,
                                bindBinding = { ItemLayoutBinding.bind(it) },
                                binder = { bind, item, _ ->
                                    bind.content = "this is some item"
                                },
                                recycleFun = { it.model = null; it.content = null; it.click = null })
                                .forList()
                ))
        .add(provideData(index).let { HListK.singleId(it).putF(it) },
                ComposeRenderer.startBuild
                        .add(LayoutRenderer<ItemModel>(layout = R.layout.simple_item,
                                stableIdForItem = { item, index -> item.id },
                                binder = { view, itemModel, index -> view.findViewById<TextView>(R.id.simple_text_view).text = itemModel.title },
                                recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" })
                                .forList({ i, index -> index }))
                        .add(databindingOf<ItemModel>(R.layout.item_layout)
                                .onRecycle(CLEAR_ALL)
                                .itemId(BR.model)
                                .itemId(BR.content, { m -> m.content + "xxxx" })
                                .stableIdForItem { it.id }
                                .forList())
                        .build())
        .add(DateFormat.getInstance().format(Date()),
                databindingOf<String>(R.layout.list_footer)
                        .itemId(BR.text)
                        .forItem())
        .build()

以上代码实现了一个混合Adapter的建立:ide

|--LayoutRenderer  header
|
|--SealedItemRenderer
|    |--none -> LayoutRenderer placeholder count 5
|    | 
|    |--some -> ListRenderer
|                 |--DataBindingRenderer 1
|                 |--DataBindingRenderer 2
|                 |--... 
|
|--ComposeRenderer
|    |--ListRenderer
|    |   |--LayoutRenderer simple item1
|    |   |--LayoutRenderer simple item2
|    |   |--...
|    |
|    |--ListRenderer
|        |--DataBindingRenderer item with content1
|        |--DataBindingRenderer item with content2
|        |--...
|
|--DataBindingRenderer footer

即: Build Dsl --> Adapter, 最后生成了一个混合的val adapter
而在使用的时候但愿能经过这个val adapter对结构中某些部分进行部分更新

好比上面构造的结构中, 咱们但愿只在ComposeRenderer中第二个ListRendererinsert 一个元素进去, 并合理调用Adapter的notifyItemRangeInserted(position, count)方法, 而且但愿这个操做能够经过Dsl的方式实现, 好比:

adapter.updateNow {
    // 定位ComposeRenderer
    getLast2().up {
        // 定位第二个ListRenderer
        getLast1().up {
            insert(2, listOf(ItemModel(189, "Subs Title1", "subs Content1")))
        }
    }
}

以上Dsl必然是但愿有必定的限定的, 好比不能在只有两个元素的Adapter中getLast3(), 也不能在非列表中执行insert()

而这些限制须要被从val adapter推出, 即adapter --> Update Dsl, 这意味着adapter中须要保存其结构的全部信息, 因为咱们须要在编译期对结构信息进行提取, 也意味着应该在类型信息中保存全部的结构信息

对于一般的Renderer没有太大的问题, 但对于部分组合其余Renderer的Renderer, (好比ComposeRenderer, 它的做用是按顺序将任意的Renderer组合在一块儿), 一般的实现方式是将他们通通还原为共通父类(BaseRenderer), 而后看作一样的东西进行操做, 但这个还原操做也同时将各自独特的类型信息给丢失了, 那应该怎么办才能即保证组合的多样性, 同时又不会丢失各自的类型信息?

换一种方式描述问题

推广到其余领域, 这个问题实际挺常见的, 好比:

咱们如今有一个用于绘制的基类RenderableBase, 而有两个实现, 一个是绘制圆形的Circle和绘制矩形的Rectangle:

graph TB
A[RenderableBase]
A1[Circle]
A2[Rectangle]

A --> A1
A --> A2

咱们有一个共通的用于绘制的类Canvas, 保存有全部须要绘制的RenderableBase, 通常状况下咱们会经过一个List<RenderableBase>容器的方式保存它们, 将它们还原为通用的父类

但这种方式的问题是这种容器的类型信息中已经丢失了每一个元素各自的特征信息, 咱们无法在编译期知道或者限定子元素的类型(好比咱们并不知道其中有多少个Circle, 也不能限定第一个元素必须为Rectangle)

那是否有办法即保证容器的多样性, 同时又不会丢失各自的类型信息?

再换一种方式描述问题

对于一个函数(方法), 好比:

fun test(s: String): List<String>

它其实能够看作声明了两个部分的函数:

  1. 值函数: 描述了元素s到列表list的态射
  2. 类型函数: 描述了从类型String到类型List<String>的态射

即包括s -> listString -> List<String>

通常而言这二者是同步的, 或者说类型信息中包括了足够的值相关的信息(值的类型), 但请注意如下函数:

fun test2(s: String, i: Int): List<Any?> = listOf(s, i)

它声明了(s, i) -> list(String, Int) -> List<Any?>, 它没有将足够的类型信息保存下来:

  1. List中只包括StringInt两种元素
  2. List的Size为2
  3. List中第一个元素是String, 第二个元素是Int

那是否有办法将以上这些信息也合理的保存到容器的类型中呢?

一种解决方案

异构列表

以上的问题注意缘由是在于List容器自己, 它自己就是一个保存相同元素的容器, 而咱们须要是一个能够保存不一样元素的容器

Haskell中有一种这种类型的容器: Heterogeneous List(异构列表), 就实现上来讲很简单:

Tip: arrow中的实现
sealed class HList

data class HCons<out H, out T : HList>(val head: H, val tail: T) : HList()

object HNil : HList()

咱们来看看使用它来构造上一节咱们所说的函数应该如何构造:

// 原函数
fun test2(s: String, i: Int): List<Any?> = listOf(s, i)

// 异构列表
fun test2(s: String, i: Int): HCons<Int, HCons<String, HNil>> =
  HCons(i, HCons(s, HNil))

一样是构建列表, 异构列表包含了更丰富的类型信息:

  1. 容器的size为2
  2. 容器中第一个元素为String, 第二个为Int

相比传统列表异构列表的优点

  1. 完整保存全部元素的类型信息
  2. 自带容器的size信息
  3. 完整保存每一个元素的位置信息

好比, 咱们能够限定只能传入一个保存两个元素的列表, 其中第一个元素是String, 第二个是Int:

fun test(l: HCons<Int, HCons<String, HNil>>)

同时咱们也能够肯定第几个元素是什么类型:

val l: HCons<Int, HCons<String, HNil>> = ...

l.get0() // 此元素必定是Int类型的

因为Size信息被固定了, 传统必须在运行期才能检查的下标是否越界的问题也能够在编译期被检查出来:

val l: HCons<Int, HCons<String, HNil>> = ...

l.get3() // 编译错误, 由于只有两个元素

相比传统列表的难点

  1. 因为Size信息和元素类型信息是绑定的, 抛弃Size信息的同时就会抛弃元素类型的限制
  2. 注意类型信息中的元素信息和实际保存的元素顺序是相反的, 由于异构列表是一个FILO(先进后出)的列表
  3. 因为Size信息是限定的, 针对不一样Size的列表的处理须要分开编写

对于第一点, 以上面的RenderableBase为例, 好比咱们有一个函数能够处理任意Size的异构列表:

fun <L : HList> test(l: L)

咱们反而没法限定每一个元素都应该是继承自RenderableBase的, 这意味着HCons<Int, HCons<String, HNil>>这种列表也能够传进来, 这在某些状况下是很麻烦的

异构列表中附加高阶类型的处理

Tip: 关于高阶类型的内容能够参考这篇文章 高阶类型带来了什么

继承是OOP的一大难点, 它的缺点在程序抽象度愈来愈高的过程的愈来愈凸显.
函数范式中是以组合代替继承, 使得程序有着更强的灵活性

因为采用函数范式, 咱们再也不讨论异构列表如何限定父类, 而是改成讨论异构列表如何限定高阶类型

对HList稍做修改便可附加高阶类型的支持:

Tip: DslAdapter中的详细实现: HListK
sealed class HListK<F, A: HListK<F, A>>

class HNilK<F> : HListK<F, HNilK<F>>()

data class HConsK<F, E, L: HListK<F, L>>(val head: Kind<F, E>, val tail: L) : HListK<F, HConsK<F, E, L>>()

Option(可选类型)为例:

arrow中的详细实现: Option
sealed class Option<out A> : arrow.Kind<ForOption, A>

object None : Option<Nothing>()

data class Some<out T>(val t: T) : Option<T>()

经过修改后的HListK咱们能够限定每一个元素都是Option, 但并不限定Option内容的类型:

// [Option<Int>, Option<String>]
val l: HConsK<ForOption, String, HConsK<ForOption, Int, HNilK<ForOption>>> =
  HConsK(Some("string"), HConsK(199, HNilK()))

修改后的列表便可作到即保留每一个元素的类型信息又能够对元素类型进行部分限定

它即等价于原生的HList, 同时又有更丰富的功能

好比:

// 1. 定义一个单位类型
data class Id<T>(val a: T) : arrow.Kind<ForId, A>

// 类型HListK<ForId, L>即等同于原始的HList
fun <L : HListK<ForId, L>> test()


// 2. 定义一个特殊类型
data class FakeType<T, K : T>(val a: K) : arrow.Kind2<ForFakeType, T, K>

// 便可限定列表中每一个元素必须继承自RenderableBase
fun <L : HListK<Kind<ForFakeType, RenderableBase>, L>> test(l: L) = ...

fun test2() {
    val t = FakeType<RenderableBase, Circle>(Circle())
    val l = HListK.single(t)

    test(l)
}

回到DslAdapter的实现

上文中提到的异构列表已经足够咱们用来解决文章开头的DslAdapter实现问题了

异构问题解决起来就很是瓜熟蒂落了, 以ComposeRenderer为例, 咱们使用将子Renderer装入ComposeItem容器的方式限定传入的容器每一个元素必须是BaseRenderer的实现, 同时ComposeItem经过泛型的方式尽最大可能保留Renderer的类型信息:

data class ComposeItem<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>(
        val renderer: BR
) : Kind<ForComposeItem, Pair<T, BR>>

其中能够注意到类型声明中的Kind<ForComposeItem, Pair<T, BR>>, arrow默认的三元高阶类型为Kind<Kind<ForComposeItem, T>, BR>, 这并不符合咱们在这里对高阶类型的指望: 咱们这里只想限制ForComposeItem, 而T咱们但愿和BR绑定在一块儿限定, 因此使用了积类型 Pair将T和BR两个类型绑定到了一块儿. 换句话说, Pair在这里只起到一个组合类型T和BR的类型粘合剂的做用, 实际并不会被使用到

ComposeItem保存的是在build以后不会改变的数据(好比Renderer), 而使用中会改变的数据以ViewData的形式保存在ComposeItemData:

data class ComposeItemData<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>(
        val viewData: VD,
        val item: ComposeItem<T, VD, UP, BR>) : Kind<ForComposeItemData, Pair<T, BR>>

这里一样使用了Pair做为类型粘结剂的技巧

对于一个ComposeRenderer而言应该保存如下信息:

  1. 能够渲染的数据类型
  2. 子Renderer的全部类型信息
  3. 当前Renderer的ViewData信息以及子Renderer的ViewData信息

其中

  • 2. 子Renderer的全部类型信息IL : HListK<ForComposeItem, IL>泛型信息保存
  • 3. 当前Renderer的ViewData信息以及子Renderer的ViewData信息VDL : HListK<ForComposeItemData, VDL>泛型信息保存
  • 1. 能够渲染的数据类型DL : HListK<ForIdT, DL>(ForIdT等同于上文提到的单位类型Id)

因而咱们能够获得ComposeRenderer的类型声明:

class ComposeRenderer<DL : HListK<ForIdT, DL>, IL : HListK<ForComposeItem, IL>, VDL : HListK<ForComposeItemData, VDL>>

子Renderer的全部类型信息(Size, 下标等等)被完整保留, 也就意味着从类型信息咱们能够还原出每一个子Renderer的完整类型信息

一个栗子:
构造两个子Renderer:

// LayoutRenderer
val stringRenderer = LayoutRenderer<String>(layout = R.layout.simple_item,
        count = 3,
        binder = { view, title, index -> view.findViewById<TextView>(R.id.simple_text_view).text = title + index },
        recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" })
        
// DataBindingRenderer
val itemRenderer = databindingOf<ItemModel>(R.layout.item_layout)
        .onRecycle(CLEAR_ALL)
        .itemId(BR.model)
        .itemId(BR.content, { m -> m.content + "xxxx" })
        .stableIdForItem { it.id }
        .forItem()

使用ComposeRenderer组合两个Renderer:

val composeRenderer = ComposeRenderer.startBuild
        .add(itemRenderer)
        .add(stringRenderer)
        .build()

你能够猜出这里composeRenderer的类型是什么吗?

答案是:

ComposeRenderer<
  HConsK<ForIdT, String, HConsK<ForIdT, ItemModel, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<String, LayoutRenderer<String>>,
  HConsK<ForComposeItem, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItem>>>,
  HConsK<ForComposeItemData, Pair<String, LayoutRenderer<String>>, HConsK<ForComposeItemData, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItemData>>>
>

其中完整保留了全部咱们须要的类型信息, 所以咱们能够经过composeRenderer还原出原来的数据结构:

composeRenderer.updater
        .updateBy {
            getLast1().up {
                update("New String")
            }
        }

这里的update("New String")方法知道当前定位的是一个stringRenderer, 因此可使用String更新数据, 若是传入ItemModel就会出错

虽然泛型信息很是多而长, 但实际大部分能够经过编译系统自动推测出来, 而对于某些没法被推测的部分也能够经过一些小技巧来简化, 你能够猜到用了什么技巧吗?

结语

之前咱们经常更聚焦于面向过程编程, 但对函数范式或者说Haskell的学习, 类型编程其实也是一个颇有趣而且颇有用的思考方向

没错, 类型是有相应的计算规则的, 甚至有的编程语言会将类型做为一等对象, 能够进行相互计算(积类型, 和类型, 类型的幂等)

虽然Java或者Kotlin的类型系统并无如此的强大, 但只要改变一下思想, 经过一些技巧仍是能够实现不少像魔法同样的事情(好比另外一篇文章中对高阶类型的实现)

将Haskell的对类型系统编程应用到Kotlin上有不少有趣的技巧, DslAdapter只是在实用领域上一点小小的探索, 而fpinkotlin则是在实验领域的另一些探索成果(尤为是第四部分 15.流式处理与增量I/O), 但愿以后能有机会分享更多的一些技巧和经验, 也欢迎感兴趣的朋友一同探讨

相关文章
相关标签/搜索