YRoute是一個新開發的Android路由庫,使用了Arrow函數式庫做爲核心庫,是以前對於函數範式學習和思考的集大成者。但目前還在前期開發階段,僅實現了一些簡單的功能作架構驗證用。java
OOP中的23種設計模式相信你們已經爛熟於心了, 它們已經被普遍應用於軟件工業的各個領域. 它們當初被創造是由於當時舊的編程思想在軟件規模逐漸龐大的狀況下已經難以駕馭了. 然而隨着軟件工業這麼多年的持續發展, 一樣的問題又來到了OOP的面前, 如今的代碼抽象度愈來愈高, OOP的不少技法已經開始有點捉襟見肘, 這也是爲何這幾年抽象度更高的函數範式的概念被愈來愈多的提起
函數式編程中單子、高階類型等概念被常常提起, 但面向組合子編程
的概念卻少有說起, 它是一種與之前構建程序徹底的不一樣的思惟模式: 由下至上構建程序.
YRoute是使用這種方式進行構建的, 但願經過這個庫和這篇對於開發過程描述的文章, 對你們會有所啓發
FragmentManager是幾年前我的開發的一個Fragment管理庫,相比其餘庫有Rx方式啓動、多堆棧切換、Fragment與Activity一致的動畫處理等等。此庫在多個實際項目中被使用,功能被不斷完善,穩定性、靈活性也獲得了項目的驗證,因此如今基本是項目開發的默認基礎庫了。 git
但對於我的而言其實一直不滿於這個庫自己的架構技術。最開始構建的時候以功能實現爲主,也以之前習慣的OOP思想進行構建(由於那個時候尚未被函數範式「荼毒」),致使了架構上的各類問題:github
BaseFragmentManagerActivity
和BaseManagerFragment
BaseFragmentManagerActivity
被設計爲了「超級類」:功能強大,但包含了大量能夠被分離的邏輯,致使邏輯代碼混雜SwipeBackUtil
、Rx啓動功能、抖動抑制ThrottleUtil
),但基於OOP設計自己的缺點,它們的分離沒有統一的模式,也沒法真正清晰的分離,實際功能代碼仍是須要依賴混雜到BaseFragmentManagerActivity
得益於對函數範式在實踐中的更多理解, 纔有了YRoute這個庫的出現編程
要理解YRoute庫, 首先須要介紹一下相關的幾個數據結構設計模式
這是一個用於類型轉換的數據類型, 從它的定義上就能夠看出網絡
data class Lens<S, T>( get: (S) -> T, set: (S, T) -> S )
它包含兩個函數, 一個是從數據類型S中提取T的get
函數, 二一個是將舊的S和T數據組合成爲新的S的函數set
數據結構
用法能夠參照:架構
data class Player(val health: Int) val playerLens: Lens<Player, Int> = Lens( get = { player -> player.health }, set = { player, value -> player.copy(health = value) } ) val player = Player(70)
在函數範式中咱們會提取不少的單子, 而其中函數自己其實也是一種單子, 而函數(D) -> A
所抽取的單子就是Reader
:異步
class Reader<D, A>(val run: (D) -> A)
Reader的意思能夠理解爲從數據類型D中讀取A數據
函數式編程
它還有個高階類型的版本ReaderT
:
class ReaderT<F, D, A>(val run: (D) -> Kind<F, A>)
這個版本以後會使用到
State
正如其名, 是狀態機的高級抽象, 本質上而言它就是(S) -> Pair<S, A>
函數, 便是表示輸入舊的狀態, 返回一個新狀態並獲得一個值A
, 每運行一次便表明狀態機狀態的一次轉化
它也有一個高階類型的版本StateT
:
data class StateT<F, S, A>(val run: (S) -> Kind<F, Pair<S, A>>)
返回的不是純粹的元組Pair<S, A>
, 而是一個被單子F包裹的元組
這裏的IO
不一樣於Java或者其餘語言中所簡單表明的Input/Output
, 函數範式要解決的核心問題是如何去掉反作用
, 然而反作用是程序中必須的存在, 輸入/輸出就是典型的反作用, 程序都是經過一系列輸入產生一系列輸出而運行的. 因此函數範式或者說Haskell中不是去掉
反作用, 而是隔離
反作用, 經過類型的方式. 而IO
數據類型就是用於描述、包裹反作用的單子, 能夠認爲看到IO類型就知道里面是帶反作用的, 而組合IO類型或者沒有IO類型的話就是無反作用的純函數
IO數據類型自己是純的, 組合它也是純的, 只有在最後執行它的時候會產生反作用, 即unsafeRunAsync
、unsafeRunSync
等方法, 能夠看到這些執行方法前面都有一個unsafe
前綴, 由於這些方法都不是純函數, 由於它們執行了反作用. 也正因如此, 一般只會在一個地方使用這個方法, 那就是入口函數main
—
理解了以上一些基礎類型以後, 能夠開始進入YRoute庫了
YRoute的核心其實很簡單, 就是類型YRoute<S, R>
, 能夠看一下庫中對它的定義:
typealias YRoute<S, R> = StateT<ReaderTPartialOf<ForIO, RouteCxt>, S, YResult<R>>
因爲Kotlin類型系統不夠強大, 只能這樣描述. 結合咱們上面對其中使用的幾種數據類型講解, 實際上YRoute
類型能夠看做:
(S, RouteCxt) -> IO<Pair<S, YResult<R>>>
其中RouteCxt
是YRoute中定義的上下文數據, 因此它能夠看做: 輸入舊狀態S和上下文RouteCxt, 輸出包裹反作用的IO類型, 其中IO返回的值爲新的狀態S和運行結果YResult<R>
這是對路由
這種業務邏輯的高階抽象: 路由就是對上下文進行操做(好比啓動Activity或者管理Fragment等)而後將一些額外的狀態進行變換, 獲得一個新的狀態以及運行結果(結果多是失敗或者成功)
那麼上面的YRoute類型最開始是如何被肯定的呢
咱們首先來分析一下, 路由
本質上是一個狀態機: 獲取當前狀態執行某個動做,返回新的狀態並返回一個值。好比說關閉一個Activity就是: 獲取當前Activity棧狀態,關閉目標的Activity,把Activity棧中的這個Activity刪除,返回新的棧和執行結果(若是沒有找到目標Activity則返回失敗)
所以按照上面介紹的數據結構,咱們首先選用State
// (S) -> Pair<S, R> State<S, R>
但路由與普通的狀態機不一樣,它一般是伴隨着UI操做的,即會有反作用發生,所以咱們須要將其包裹於IO
類型中
// (S) -> IO<Pair<S, R>> StateT<ForIO, S, R>
同時,路由還有一個特性: 能夠在任意位置調用。這意味着在調用它的時候咱們是不能依賴於運行上下文的。換句話說,咱們須要將運行上下文傳入。咱們知道傳入
函數(T) -> R
的抽象爲Reader
,因此能夠獲得:
// (S) -> (RouteCxt) -> IO<Pair<S, R>> StateT<ReaderTPartialOf<ForIO, RouteCxt>, S, R>
基本雛形已經出來了,這已經和最初的發佈版很類似了,但這時候有一個關於返回值的問題咱們須要思考:返回值R一般咱們指定的是最後成功的話返回的值。意味着只要能拿到R值,就必須執行成功。而目前咱們失敗
的語義靠的是IO
類型,而IO
類型同時也包裹着新的狀態S。
那麼就存在這麼一種狀況:好比咱們但願實現連續關閉兩個Activity
的動做,而咱們關閉第一個成功了,但第二個關閉失敗了(好比沒有找到),但實際上這個時候狀態已經改變了(第一個Activity已經被關閉了,因此須要被刪除),但因爲第二個的失敗咱們的IO只能處於失敗
狀態,外部沒法獲取到新的狀態S, 狀態就沒法被改變,這是不可接受的
這種狀況在實際運行中會常常發生:咱們路由的運行結果確定不必定是成功的。這個問題的核心是:咱們把IO的異常
和運行結果的失敗
放在一塊兒了。因此解決方式是須要將它們分開,定義一個新的代數數據類型YResult
(前面加個Y是爲了和原生的其餘Result類型區分開),它有兩種狀態,Success
和Fail
,因而咱們最終的組合子定義就出來了:
// (S) -> (RouteCxt) -> IO<Pair<S, YResult<R>>> StateT<ReaderTPart<ForIO, RouteCxt>, S, YResult<R>>
函數式編程是經過組合而非繼承的方式擴展類型能力的
其實最開始是使用了YRoute<S, P, R>的數據類型,即在Route構建時便固定了輸入參數P的類型。這是基於最開始提取核心類型時建模爲 S -> (P -> R), 即但願最終Route的運行結果是一個P -> R的函數, 因此類型是
// (S) -> (RouteCxt) -> IO<Pair<S, YResult<(P) -> R>>> StateT<ReaderTPartialOf<ForIO, Tuple2<RouteCxt, P>>, S, Result<R>>
能夠看到最初構建的時候相比如今多了一個P
類型, 做爲路由輸入參數的表示
這個版本的YRoute的3個類型是有不一樣的變換方式的:
S:S1到S2的狀態變換須要 S1 -> S2和S1 <- S2兩個函數(使用Lens
類型進行描述)
P:P1變換到P2須要P1 <- P2函數
R:R1變換到R2須要R1 -> R2函數
因爲它們的變換方向徹底不一樣,會致使這時YRoute進行組合的時候很是複雜,一般須要考慮三種狀態的分別變換,若是相互之間還須要提取轉換的話會變得更加複雜。
而這種複雜是否真的值得呢?不必定。
追尋保留P類型的最開始初衷,是但願P能夠被延遲(lazy)
提供,這樣可使得Route的構建和Route的使用徹底的分開。但實際上有些Route構建時須要的參數只是一些中間參數,並不須要保留到外面;同時這樣作使得Route須要的參數能夠被放在函數參數中(如startActivity(builder: Builder)
)、也能夠放在YRoute範型中(如YRoute<S, Builder, R>
),兩種方式好像同樣又好像有點區別,容易引發混淆,而這兩種方式使用和變換上由有些不一樣,影響了使用的靈活性;而實際上P
這個類型和Route路由運行時沒有關係,核心運行時Core
的最終做用是執行Route並返回R
,它並不關心P,因此實際上P必須在放進Core執行前就被固定
住。
而最終改變YRoute類型定義的最核心一個緣由是:做爲延遲參數類型的P
實際是YRoute<S, R>類型的一個加強類型而已。回到最上面說的YRoute<S, P, R>
類型的函數表示:
(S) -> (RouteCxt, P) -> IO<S, Result<R>>
稍微變換一下參數順序:
(P) -> (RouteCxt, S) -> IO<S, Result<R>>
對於只關心結果IO<S, Result<R>>
的咱們而言它們是等效
,因此咱們徹底能夠將YRoute定義爲:YRoute<S, R> = (RouteCxt, S) -> IO<S, Result<R>>
,而定義LazyYRoute<S, P, R> = (P) -> YRoute<S, R>
這樣Route的定義能夠再也不考慮P的變換,變得更加自由、簡單。而若是須要延遲參數
的功能使用LazyYRoute
類型包裝便可。
通過一系列的腦力運動後, 咱們肯定了咱們的核心組合子YRoute
, 這就至關於咱們的磚塊, 但咱們的目標是搭一個大樓出來, 那就來看看咱們用它能作點什麼吧.
以啓動Activity的功能爲例, Android中默認的流程是: 建立Intent、而後用startActivity方法啓動, 那麼咱們就現構造兩個個基本路由:
fun <T : Activity, VD> createActivityIntent(builder: ActivityBuilder<T>): YRoute<VD, Intent> fun startActivity(intent: Intent): YRoute<ActivitiesState, Activity>
createActivityIntent
用於建立Intent; startActivity
用於經過Intent啓動新Activity. 因而能夠組合這兩個函數成爲新YRoute實現新功能:
val newRoute = createActivityIntent(builder) .flatMap { intent -> startActivity(intent) }
StackRoute 中有更復雜的組合示例
函數式編程中咱們會反覆討論反作用
, 所以一些「函數式」架構也會主打反作用隔離, 好比Redux和Flux, 它們嘗試經過分層的方式隔離掉反作用, 即中間件, 它們但願反作用只在中間件中執行, Reducer是純函數.
但實際使用過這些架構的人就會知道它是多麼的「反人類」: 一個簡單的邏輯被分散到了Controller、Action、Reducer、Middleware以及相應的State, 整個程序處處散落着界面的邏輯; 自己Action和Reducer等又有着大量的模版代碼.
這致使以前使用這些架構對FragmentManager進行重構的時候各類帶刺
這裏的根本緣由是, 它們雖然瞭解了反作用的處理對於程序的重要性, 但解決上卻仍然是使用的OOP的思惟方式. 它們是嘗試經過「分層」的方式分離反作用, 這是一種粒度很大的隔離方式, 缺少組合性
而Haskell中是使用類型IO進行反作用分離的, 正如上文所說, IO會包裹反作用, 但對IO的操做除了那些unsafe
方法其餘都是無反作用的, 因此IO能夠存在於程序的任何地方, 也能夠與其餘「純」的數據結構進行任意組合而不會破壞程序的「純度」, 這就是經過「類型」的方式進行反作用隔離
Rx系列是近幾年很是火的庫, 但它既不是ORM庫也不是網絡庫, 實際上它自己沒有任何業務邏輯, 但當在項目中使用它的時候卻能確實地感受到它與其餘庫的不同凡響, 它給程序構建帶來的全面的改變.
這是爲何呢? 或者說Rx到底是一個什麼庫?
能夠看到, 它就是函數範式中基本工具的集大成者, 它是一個函子、是一個單子、是一個IO、是一個Async等等, 它把這些功能通通集中起來, 固然最核心的是實現了Push-Pull FRP流
但反過來講, 它的這種通用和強大反而是一種不足, 它成爲了一個「超級類」: 一個類型裏面包含了過多的功能, 致使描述性下降. 這句話可能難以理解, 舉個栗子: 當咱們一個函數返回Single<Int>
的時候, 咱們能夠解讀出這些信息:
因爲不肯定, 因此咱們須要處理全部的狀況, 就像傳入一個Any
咱們就要判斷全部可能的值, 這就是描述性不足: 沒法準確表達程序的意義
Rx其實意識到了這個問題, 因此RxJava2的時候把RxJava1中「萬能」的Observable分紅了Maybe
、Single
、Completable
等, 但這樣的作法又帶來了另外一個問題
明明Maybe
和Single
都有一個叫map
的函數, 卻必須寫兩遍, 由於他們只能被視爲兩個不一樣的函數, 沒法被抽象成同一個函數
System F-sub
以上這些固然最核心的緣由是限制於Java自己語言表現力的問題, 因此沒法徹底按照函數範式的方式來實現. 反觀其餘語言, 更具語言表現力的Scala中, RxScala並不流行; 純函數式語言Haskell中根本就沒有Rx, 由於它有Reactive-Banana、Yampa等更強大的庫, 它們是更貼近FRP理論本源的實現
Rx不是一個通用異步處理工具這麼簡單, 它將函數範式的一瞥帶入了OOP中, 即帶來了極大的改變. 雖然它有一些不足, 但限於語言自己很難一下加入不少高級特性, 能作到Arrow這一步已是很是厲害了, 做爲Android開發的咱們可能很長一段時間仍是會依賴Rx
但願這篇筆記和這個庫能夠給各位一些啓發, 歡迎前來star (^ ^) YRoute