程式中時常有許多函數是需要去使用共用的常數(shared environment),常見的像是設定檔(config)之類的東西。
此時函數的參數就是這些使用到的如 config,我們必須在 type signature 寫出需要的參數型別,此外在使用時必須顯式的寫出要傳入的東西
這樣的壞處很明顯就是我們必須重複寫很多東西。
要解決這問題的一個直覺想法就是,讓這些函數所在的 scope,可以自由地取得共用的資料,如此一來就不需由我們手動傳入了。
而在 functional programming 中,就是使用函數去達成。只要將這些函數做良好的定義,並塞到一個更大的函數裡,將共享資源傳入這個大函數中,裡頭的小函數們就都可以去取得了。
Reader monad 就是可以幫我們做這件事情的工具,因此他又有另外的名稱,Environment monad。
而最簡單的 Reader monad 其實就是一個函數的新名稱罷了。
範例
這裡用一段沒什麼用處的程式碼來舉例。
這裡的函數用返回執行別用Maybe,只是為了要用do notation而已
可以發現 nameAndAge, revName, weightAndHeight, calBmi等函數,我們需要定義、傳地重複的東西進去,使程式碼看起來較為繁冗。
而他們用到的這些 input 就可以將其想像成 shared environment 這種在程式執行時並不會去改變且很多地方都會用到的東西。
定義 Reader monad
|
|
也就是說,Reader 就是包裹了一個函數 r -> a ,其中 r 就是我們要傳入的共用資料,有些地方也會用e來表示(environment);a 表示吃進 r 後,會回傳的東西是什麼。
再來我們可以自己定義他一系列的Functor Applicative Monad instance
|
|
可以看到這裡的 structure 是 Reader r 也就是 (->) r (function type) 這部分。而且在實作當中可以看到,r是保持著原來的值被傳遞,並沒有被做其他transform,因此可以確保 Reader monad 可以拿到相同的r
對照 (->) r 在原始碼中定義的Functor Applicative Monad instance,可以發現基本上是一樣的,只是多了 Reader 這一層包裝。
若將型別定義中的 Reader r 用 (->) r 替換,可以看到更明顯的結果
所以其實 Reader monad 的概念,就是跟 function type (->) r 是一樣的。
Methods
使用了newtype Reader將 function 包裝後,還沒有什麼用。
還需要一些小工具使我們可以方便地拿到想要的資料,ask與asks以及local
ask就是一個id,也就是傳什麼就吐一樣的東西回來。所以就是可以拿到Reader r a中的r。
asks的參數是一個函數,這個函數的作用就類似 selector,用來塞選取得在 shared environment 中想要的資料。
local是用來修改 Reader content,但他不是修改全域的內容,而是只有在local scope 裡面的 Reader Monad 才會被影響。
從ex的執行範例可以看到,i也就是經過local後 Reader content 的確是被修改了,但是用ask拿到的 Reader content j,還是為 10 沒有改變。
所以local的作用是區域修改而不會影響到其 scope 外的地方,所以總的來說其實 Reader content並沒有發生改變。
範例改寫
原本的範例程式可以用 Reader monad 如此改寫
主要的差別在於,每個函數的 type signature 是一致的 Reader Config [回傳型別],大家是共享一份 config。
各函數在其內部自行定義要取得什麼資料,如此一來在使用的時候就不需要顯示的傳遞進去了。
結語
一般情況在使用 Reader Monad 時並不會自行去定義,可能會用一些 library 像 mtl或transformers,
他們同樣也都提供了基本的像是ask, asks, local,可以去看看這些 library 是如何實作的,藉此來學習。
此外通常看到或是使用時,因為會需搭配其他的 Monad 一起使用,並不會用Reader而是使用ReaderT,T表示 transformer。