異構列表(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), 但願以後能有機會分享更多的一些技巧和經驗, 也歡迎感興趣的朋友一同探討

相關文章
相關標籤/搜索