<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 第十八章: Monad變換器 ## 動機: 避免樣板代碼 Monad提供了一種強大途徑以構建帶效果的計算。雖然各個標準monad皆專一于其特定的任務,但在實際代碼中,我們常常想同時使用多種效果。 比如,回憶在第十章中開發的 Parse 類型。在介紹monad之時,我們提到這個類型其實是喬裝過的 State monad。事實上我們的monad比標準的 State monad 更加復雜:它同時也使用了 Either 類型來表達解析過程中可能的失敗。在這個例子中,我們想在解析失敗的時候就立刻停止這個過程,而不是以錯誤的狀態繼續執行解析。這個monad同時包含了帶狀態計算的效果和提早退出計算的效果。 普通的 State monad不允許我們提早退出,因為其只負責狀態的攜帶。其使用的是 fail 函數的默認實現:直接調用 error 拋出異常 - 這一異常無法在純函數式的代碼中捕獲。因此,盡管 State monad似乎允許錯誤,但是這一能力并沒有什么用。(再次強調:請盡量避免使用 fail 函數!) 理想情況下,我們希望能使用標準的 State monad,并為其加上實用的錯誤處理能力以代替手動地大量定制各種monad。雖然在 mtl 庫中的標準monad不可合并使用,但使用庫中提供了一系列的 *monad變換器* 可以達到相同的效果。 Monad變換器和常規的monad很類似,但它們并不是獨立的實體。相反,monad變換器通過修改其以為基礎的monad的行為來工作。 大部分 mtl 庫中的monad都有對應的變換器。習慣上變換器以其等價的monad名為基礎,加以 T 結尾。 例如,與 State 等價的變換器版本稱作 StateT ; 它修改下層monad以增加可變狀態。此外,若將 WriterT monad變換器疊加于其他(或許不支持數據輸出的)monad之上,在被monad修改后的的monad中,輸出數據將成為可能。 [注:mtl 意為monad變換器函數庫(Monad Transformer Library)] [譯注:Monad變換器需要依附在一已有monad上來構成新的monad,在接下來的行文中將使用“下層monad”來稱呼monad變換器所依附的那個monad] ## 簡單的Monad變換器實例 在介紹monad變換器之前,先看看以下函數,其中使用的都是之前接觸過的技術。這個函數遞歸地訪問目錄樹,并返回一個列表,列表中包含樹的每層的實體個數: ~~~ -- file: ch18/CountEntries.hs module CountEntries ( listDirectory , countEntriesTrad ) where import System.Directory (doesDirectoryExist, getDirectoryContents) import System.FilePath ((</>)) import Control.Monad (forM, liftM) listDirectory :: FilePath -> IO [String] listDirectory = liftM (filter notDots) . getDirectoryContents where notDots p = p /= "." && p /= ".." countEntriesTrad :: FilePath -> IO [(FilePath, Int)] countEntriesTrad path = do contents <- listDirectory path rest <- forM contents $ \name -> do let newName = path </> name isDir <- doesDirectoryExist newName if isDir then countEntriesTrad newName else return [] return $ (path, length contents) : concat rest ~~~ 現在看看如何使用 Writer monad 實現相同的目標。由于這個monad允許隨時記下數值,所以并不需要我們顯示地去構建結果。 為了遍歷目錄,這個函數必須在 IO monad中執行,因此我們無法直接使用 Writer monad。但我們可以用 WriterT 將記錄信息的能力賦予 IO 。一種簡單的理解方法是首先理解涉及的類型。 通常 Writer monad有兩個類型參數,因此寫作 Writerwa 更為恰當。其中參數 w 用以指明我們想要記錄的數值的類型。而另一類型參數 a 是monad類型類所要求的。因此 Writer[(FilePath,Int)]a 是個記錄一列目錄名和目錄大小的writer monad。 WriterT 變換器有著類似的結構。但其增加了另外一個類型參數 m :這便是下層monad,也是我們想為其增加功能的monad。 WriterT 的完整類型簽名是 Writerwma。 由于所需的目錄遍歷操作需要訪問 IO monad,因此我們將writer功能累加在 IO monad之上。通過將monad變換器與原有monad結合,我們得到了類型簽名: WriterT[(FilePath,Int)]IOa 這個monad變換器和monad的組合自身也是一個monad: ~~~ -- file: ch18/CountEntriesT.hs module CountEntriesT ( listDirectory , countEntries ) where import CountEntries (listDirectory) import System.Directory (doesDirectoryExist) import System.FilePath ((</>)) import Control.Monad (forM_, when) import Control.Monad.Trans (liftIO) import Control.Monad.Writer (WriterT, tell) countEntries :: FilePath -> WriterT [(FilePath, Int)] IO () countEntries path = do contents <- liftIO . listDirectory $ path tell [(path, length contents)] forM_ contents $ \name -> do let newName = path </> name isDir <- liftIO . doesDirectoryExist $ newName when isDir $ countEntries newName ~~~ 代碼與其先前的版本區別不大,需要時 liftIO 可以將 IO monad暴露出來;同時, tell 可以用以記下對目錄的訪問。 為了執行這一代碼,需要選擇一個 WriterT 的執行函數: ~~~ ghci> :type runWriterT runWriterT :: WriterT w m a -> m (a, w) ghci> :type execWriterT execWriterT :: Monad m => WriterT w m a -> m w ~~~ 這些函數都可以用以執行動作,移除 WriterT 的包裝,并將結果交給其下層monad。其中 runWriterT 函數同時返回動作結果以及在執行過程獲得的記錄。而 execWriterT 丟棄動作的結果,只將記錄返回。 因為沒有 IOT 這樣的monad變換器,所以此處我們在 IO 之上使用 WriterT 。一旦要用 IO monad和其他的一個或多個monad變換器結合, IO 一定在monad棧的最底下。 [譯注:“monad棧”由monad和一個或多個monad變換器疊加而成,形成一個棧的結構。若在monad棧中需要 IO monad,由于沒有對應的monad變換器( IOT ),所以 IO monad只能位于整個monad棧的最底下。此外, IO 是一個很特殊的monad,它的 IOT 版本是無法實現的。] ## Monad和Monad變換器中的模式 在 mtl 庫中的大部分monad與monad變換器遵從一些關于命名和類型類的模式。 為說明這些規則,我們將注意力聚焦在一個簡單的monad上: reader monad。 reader monad的具體API位于 MonadReader 中。大部分 mtl 中的monad都有一個名稱相對的類型類。例如 MonadWriter 定義了writer monad的API,以此類推。 ~~~ -- file: ch18/Reader.hs {-# LANGUAGE FunctionalDependencies #-} class Monad m => MonadReader r m | m -> r where ask :: m r local :: (r -> r) -> m a -> m a ~~~ 其中類型變量 r 表示reader monad所附帶的不變狀態, Readerr monad是個 MonadReader 的實例,同時 ReaderTrm monad變換器也是一個。這個模式同樣也在其他的 mtl monad中重復著: 通常有個具體的monad,和其對應的monad變換器,而它們都是相應命令的類型類的實例。這個類型類定義了功能相同的monad的API。 回到我們reader monad的例子中,我們之前尚未討論過 local 函數。通過一個類型為 r->r 的函數,它可臨時修改當前的環境,并在這一臨時環境中執行其動作。舉個具體的例子: ~~~ -- file: ch18/LocalReader.hs import Control.Monad.Reader myName step = do name <- ask return (step ++ ", I am " ++ name) localExample :: Reader String (String, String, String) localExample = do a <- myName "First" b <- local (++"dy") (myName "Second") c <- myName "Third" return (a,b,c) ~~~ 若在 ghci 中執行 localExample ,可以觀察到對環境修改的效果被限制在了一個地方: ~~~ ghci> runReader localExample "Fred" Loading package mtl-1.1.0.1 ... linking ... done. ("First, I am Fred","Second, I am Freddy","Third, I am Fred") ~~~ 當下層monad m 是一個 MonadIO 的實例時, mtl 提供了關于 ReaderTrm 和其他類型類的實例,這里是其中的一些: ~~~ -- file: ch18/Reader.hs instance (Monad m) => Functor (ReaderT r m) where ... instance (MonadIO m) => MonadIO (ReaderT r m) where ... instance (MonadPlus m) => MonadPlus (ReaderT r m) where ... ~~~ 再次說明:為方便使用,大部分的 mtl monad變換器都定義了諸如此類的實例。 ## 疊加多個Monad變換器 之前提到過,在常規monad上疊加monad變換器可得到另一個monad。由于混合的結果也是個monad,我們可以憑此為基礎再疊加上一層monad變換器。事實上,這么做十分常見。但在什么情況下才需要創建這樣的monad呢? - 若代碼想和外界打交道,便需要 IO 作為這個monad棧的基礎。否則普通的monad便可以滿足需求。 - 加上一層 ReaderT ,以添加訪問只讀配置信息的能力。 - 疊加上 StateT ,就可以添加可修改的全局狀態。 - 若想得到記錄事件的能力,可以添加一層 WriterT 。 這個做法的強大之處在于:我們可以指定所需的計算效果,以量身定制monad棧。 舉個多重疊加的moand變換器的例子,這里是之前開發的 countEntries 函數。我們想限制其遞歸的深度,并記錄下它在執行過程中所到達的最大深度: ~~~ -- file: ch18/UglyStack.hs import System.Directory import System.FilePath import System.Monad.Reader import System.Monad.State data AppConfig = AppConfig { cfgMaxDepth :: Int } deriving (Show) data AppState = AppState { stDeepestReached :: Int } deriving (Show) ~~~ 此處使用 ReaderT 來記錄配置數據,數據的內容表示最大允許的遞歸深度。同時也使用了 StateT 來記錄在實際遍歷過程中所達到的最大深度。 ~~~ -- file: ch18/UglyStack.hs type App = ReaderT AppConfig (StateT AppState IO) ~~~ 我們的變換器以 IO 為基礎,依次疊加 StateT 與 ReaderT 。在此例中,棧頂是 ReaderT 還是 WriterT 并不重要,但是 IO 必須作為最下層monad。 僅僅幾個monad變換器的疊加,也會使類型簽名迅速變得復雜起來。故此處以 type 關鍵字定義類型別名,以簡化類型的書寫。 ## 缺失的類型參數呢? 或許你已注意到,此處的類型別名并沒有我們為monad類型所常添加的類型參數 a: ~~~ -- file: ch18/UglyStack.hs type App2 a = ReaderT AppConfig (StateT AppState IO) a ~~~ 在常規的類型簽名用例下, App 和 App2 不會遇到問題。但如果想以此類型為基礎構建其他類型,兩者的區別就顯現出來了。 例如我們想另加一層monad變換器,編譯器會允許 WriterT[String]Appa 但拒絕 WriterT[String]App2a 。 其中的理由是:Haskell不允許對類型別名的部分應用。 App 不需要類型參數,故沒有問題。另一方面,因為 App2 需要一個類型參數,若想基于 App2 構造其他的類型,則必須為這個類型參數提供一個類型。 這一限制僅適用于類型別名,當構建monad棧時,通常的做法是用 newtype 來封裝(接下來的部分就會看到這類例子)。 因此實際應用中很少出現這種問題。 [譯注:類似于函數的部分應用,“類型別名的部分應用”指的是在應用類型別名時,給出的參數數量少于定義中的參數數量。在以上例子中, App 是一個完整的應用,因為在其定義 typeApp=... 中,沒有類型參數;而 App2 卻是個部分應用,因為在其定義 typeApp2a=... 中,還需要一個類型參數 a 。] 我們monad棧的執行函數很簡單: ~~~ -- file: ch18/UglyStack.hs runApp :: App a -> Int -> IO (a, AppState) runApp k maxDepth = let config = AppConfig maxDepth state = AppState 0 in runStateT (runReaderT k config) state ~~~ 對 runReaderT 的應用移除了 ReaderT 變換器的包裝,之后 runStateT 移除了 StateT 的包裝,最后的結果便留在 IO monad中。 和先前的版本相比,我們的修改并未使代碼復雜太多,但現在函數卻能記錄目前的路徑,和達到的最大深度: ~~~ constrainedCount :: Int -> FilePath -> App [(FilePath, Int)] constrainedCount curDepth path = do contents <- liftIO . listDirectory $ path cfg <- ask rest <- forM contents $ \name -> do let newPath = path </> name isDir <- liftIO $ doesDirectoryExist newPath if isDir && curDepth < cfgMaxDepth cfg then do let newDepth = curDepth + 1 st <- get when (stDeepestReached st < newDepth) $ put st {stDeepestReached = newDepth} constrainedCount newDepth newPath else return [] return $ (path, length contents) : concat rest ~~~ 在這個例子中如此運用monad變換器確實有些小題大做,因為這僅僅是個簡單函數,其并沒有因此得到太多的好處。但是這個方法的實用性在于,可以將其 *輕易擴展以解決更加復雜的問題* 。 大部分指令式的應用可以使用和這里的 App monad類似的方法,在monad棧中編寫。在實際的程序中,或許需要攜帶更復雜的配置數據,但依舊可以使用 ReaderT 以保持其只讀,并只在需要時暴露配置;或許有更多可變狀態需要管理,但依舊可以使用 StateT 封裝它們。 ## 隱藏細節 使用常規的 newtype 技術,便可將細節與接口分離開: ~~~ newtype MyApp a = MyA { runA :: ReaderT AppConfig (StateT AppState IO) a } deriving (Monad, MonadIO, MonadReader AppConfig, MonadState AppState) runMyApp :: MyApp a -> Int -> IO (a, AppState) runMyApp k maxDepth = let config = AppConfig maxDepth state = AppState 0 in runStateT (runReaderT (runA k) config) state ~~~ 若只導出 MyApp 類構造器和 runMyApp 執行函數,客戶端的代碼就無法知曉這個monad的內部結構是否是monad棧了。 此處,龐大的 deriving 子句需要 GeneralizedNewtypeDeriving 語言編譯選項。編譯器可以為我們生成這些實例,這看似十分神奇,究竟是如何做到的呢? 早先,我們提到 mtl 庫為每個monad變換器都提供了一系列實例。例如 IO monad實現了 MonadIO ,若下層monad是 MonadIO 的實例,那么 mtl 也將為其對應的 StateT 構建一個 MonadIO 的實例,類似的事情也發生在 ReaderT 上。 因此,這其中并無太多神奇之處:位于monad棧頂層的monad變換器,已是所有我們聲明的 deriving 子句中的類型類的實例,我們做的只不過是重新派生這些實例。這是 mtl 精心設計的一系列類型類和實例完美配合的結果。除了基于 newtype 聲明的常規的自動推導以外并沒有發生什么。 [譯注:注意到此處 newtypeMyAppa 只是喬裝過的 ReaderTAppConfig(StateTAppStateIO)a 。因此我們可以列出 MyAppa 這個monad棧的全貌(自頂向下): - ReaderTAppConfig (monad變換器) - StateTAppState (monad變換器) - IO (monad) 注意這個monad棧和 deriving 子句中類型類的相似度。這些實例都可以自動派生: MonadIO 實例自底層派生上來, MonadStateT 從中間一層派生,而 MonadReader 實例來自頂層。所以雖然 newtypeMyAppa 引入了一個全新的類型,其實例是可以通過內部結構自動推導的。] ## 練習 1. 修改 App 類型別名以交換 ReaderT 和 StateT 的位置,這一變換對執行函數 runApp 會帶來什么影響? 1. 為 App monad棧添加 WriterT 變換器。 相應地修改 runApp 。 1. 重寫 contrainedCount 函數,在為 App 新添加的 WriterT 中記錄結果。 [譯注:第一題中的 StateT 原為 WriterT ,鑒于 App 定義中并無 WriterT ,此處應該指的是 StateT ] ## 深入Monad棧中 至今,我們了解了對monad變換器的簡單運用。對 mtl 庫的便利組合拼接使我們免于了解monad棧構造的細節。我們確實已掌握了足以幫助我們簡化大量常見編程任務的monad變換器相關知識。 但有時,為了實現一些實用的功能,還是我們需要了解 mtl 庫并不便利的一面。這些任務可能是將定制的monad置于monad棧底,也可能是將定制的monad變換器置于monad變換器棧中的某處。為了解其中潛在的難度,我們討論以下例子。 假設我們有個定制的monad變換器 CustomT : ~~~ -- file: ch18/CustomT.hs newtype CustomT m a = ... ~~~ 在 mtl 提供的框架中,每個位于棧上的monad變換器都將其下層monad的API暴露出來。這是通過提供大量的類型類實例來實現的。遵從這一模式的規則,我們也可以實現一系列的樣板實例: ~~~ -- file: ch18/CustomT.hs instance MonadReader r m => MonadReader r (CustomT m) where ... instance MonadIO m => MonadIO (CustomT m) where ... ~~~ 若下層monad是 MonadReader 的實例,則 CustomT 也可作為 MonadReader 的實例: 實例化的方法是將所有相關的API調用轉接給其下層實例的相應函數。經過實例化之后,上層的代碼就可以將monad棧作為一個整體,當作 MonadReader 的實例,而不再需要了解或關心到底是其中的哪一層提供了具體的實現。 不同于這種依賴類型類實例的方法,我們也可以顯式指定想要使用的API。 MonadTrans 類型類定義了一個實用的函數 lift : ~~~ ghci> :m +Control.Monad.Trans ghci> :info MonadTrans class MonadTrans t where lift :: (Monad m) => m a -> t m a -- Defined in Control.Monad.Trans ~~~ 這個函數接受來自monad棧中,當前棧下一層的monad動作,并將這個動作變成,或者說是 *抬舉* 到現在的monad變換器中。每個monad變換器都是 MonadTrans 的實例。 lift 這個名字是基于此函數與 fmap 和 liftM 目的上的相似度的。這些函數都可以從類型系統的下一層中把東西提升到我們目前工作的這一層。它們的區別是: fmap將純函數提升到functor層次liftM將純函數提升到monad層次lift將一monad動作,從monad棧中的下一層提升到本層 [譯注:實際上 liftM 間接調用了 fmap ,兩個函數在效果上是完全一樣的。譯者認為,當操作對象是monad(所有的monad都是functor)的時候,使用其中的哪一個只是思考方法上的不同。] 現在重新考慮我們在早些時候定義的 App monad棧 (之前我們將其包裝在 newtype 中): ~~~ -- file: ch18/UglyStack.hs type App = ReaderT AppConfig (StateT AppState IO) ~~~ 若想訪問 StateT 所攜帶的 AppState ,通常需要依賴 mtl 的類型類實例來為我們處理組合工作: ~~~ -- file: ch18/UglyStack.hs implicitGet :: App AppState implicitGet = get ~~~ 通過將 get 函數從 StateT 中抬舉進 ReaderT , lift 函數也可以實現同樣的效果: ~~~ -- file: ch18/UglyStack.hs explicitGet :: App AppState explicitGet = lift get ~~~ 顯然當 mtl 可以為我們完成這一工作時,代碼會變得更清晰。但是 mtl 并不總能完成這類工作。 ## 何時需要顯式的抬舉? 我們必須使用 lift 的一個例子是:當在一個monad棧中,同一個類型類的實例出現了多次時: ~~~ -- file: ch18/StackStack.hs type Foo = StateT Int (State String) ~~~ 若此時我們試著使用 MonadState 類型類中的 put 動作,得到的實例將是 StateTInt ,因為這個實例在monad棧頂。 ~~~ -- file: ch18/StackStack.hs outerPut :: Int -> Foo () outerPut = put ~~~ 在這個情況下,唯一能訪問下層 State monad的 put 函數的方法是使用 lift : ~~~ -- file: ch18/StackStack.hs innerPut :: String -> Foo () innerPut = lift . put ~~~ 有時我們需要訪問多于一層以下的monad,這時我們必須組合 lift 調用。每個函數組合中的 lift 將我們帶到更深的一層。 ~~~ -- file: ch18/StackStack.hs type Bar = ReaderT Bool Foo barPut :: String -> Bar () barPut = lift . lift . put ~~~ 正如以上代碼所示,當需要用 lift 的時候,一個好習慣是定義并使用包裹函數來為我們完成抬舉工作。因為這種在代碼各處顯式使用lift的方法使代碼變得混亂。另一個顯式lift的缺點在于,其硬編碼了monad棧的層次細節,這將使日后對monad棧的修改變得復雜。 ## 構建以理解Monad變換器 為了深入理解monad變換器通常是如何運作的,在本節我們將自己構建一個monad變換器,期間一并討論其中的組織結構。我們的目標簡單而實用: MaybeT 。但是 mtl 庫意外地并沒有提供它。 [譯注:如果想使用現成的 MaybeT ,現在你可以在 Hackage 上的 transformers 庫中找到它。] 這個monad變換器修改monad的方法是:將下層monad ma 的類型參數包裝在 Maybe 中,以得到類型 m(Maybea) 。正如 Maybe monad一樣,若在 MaybeT monad變換器中調用 fail ,則計算將提早結束執行。 為使 m(Maybea) 成為 Monad 的實例,其必須有個獨特的類型。這里我們通過 newtype 聲明來實現: ~~~ -- file: ch18/MaybeT.hs newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) } ~~~ 現在需要定義三個標準的monad函數。其中最復雜的是 (>>=) ,它的實現也闡明了我們實際上在做什么。在開始研究其操作之前,不妨先看看其類型: ~~~ -- file: ch18/MaybeT.hs bindMT :: (Monad m) => MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b ~~~ 為理解其類型簽名,回顧之前在十五章中對“多參數類型類”討論。此處我們想使 *部分類型*MaybeTm 成為 Monad 的實例。這個部分類型擁有通常的單一類型參數 a ,這樣便能滿足 Monad 類型類的要求。 [譯注: MaybeT 的完整定義是 MaybeTma ,因此 MaybeTm 只是部分應用。] 理解以下 (>>=) 實現的關鍵在于: do 代碼塊里的代碼是在 *下層* monad中執行的,無論這個下層monad是什么。 ~~~ -- file: ch18/MaybeT.hs x `bindMT` f = MaybeT $ do unwrapped <- runMaybeT x case unwrapped of Nothing -> return Nothing Just y -> runMaybeT (f y) ~~~ 我們的 runMaybeT 函數解開了在 x 中包含的結果。進而,注意到 <- 符號是 (>>=) 的語法糖:monad變換器必須使用其下層monad的 (>>=) 。而最后一部分對 unwrapped 的結構分析( case 表達式),決定了我們是要短路當前計算,還是將計算繼續下去。最后,觀察表達式的最外層。為了將下層monad再次藏起來,這里必須用 MaybeT 構造器包裝結果。 剛才展示的 do 標記看起來更容易閱讀,但是其將我們依賴下層monad的 (>>=) 函數的事實也藏了起來。下面提供一個更符合語言習慣的 MaybeT 的 (>>=) 實現: ~~~ -- file: ch18/MaybeT.hs x `altBindMT` f = MaybeT $ runMaybeT x >>= maybe (return Nothing) (runMaybeT . f) ~~~ 現在我們了解了 (>>=) 在干些什么。關于 return 和 fail 無需太多解釋, Monad 實例也不言自明: ~~~ -- file: ch18/MaybeT.hs returnMT :: (Monad m) => a -> MaybeT m a returnMT a = MaybeT $ return (Just a) failMT :: (Monad m) => t -> MaybeT m a failMT _ = MaybeT $ return Nothing instance (Monad m) => Monad (MaybeT m) where return = returnMT (>>=) = bindMT fail = failM ~~~ ## 建立Monad變換器 為將我們的類型變成monad變換器,必須提供 MonadTrans 的實例,以使用戶可以訪問下層monad: ~~~ -- file: ch18/MaybeT.hs instance MonadTrans MaybeT where lift m = MaybeT (Just `liftM` m) ~~~ 下層monad以類型 a 開始:我們“注入” Just 構造器以使其變成需要的類型: Maybea 。進而我們通過 MaybeT 藏起下層monad。 ## 更多的類型類實例 在定義好 MonadTrans 的實例后,便可用其來定義其他大量的 mtl 類型類實例了: ~~~ -- file: ch18/MaybeT.hs instance (MonadIO m) => MonadIO (MaybeT m) where liftIO m = lift (liftIO m) instance (MonadState s m) => MonadState s (MaybeT m) where get = lift get put k = lift (put k) -- ... 對 MonadReader,MonadWriter等的實例定義同理 ... ~~~ 由于一些 mtl 類型類使用了函數式依賴,有些實例的聲明需要GHC大大放寬其原有的類型檢查規則。(若我們忘記了其中任意的 LANGUAGE 指令,編譯器會在其錯誤信息中提供建議。) ~~~ -- file: ch18/MaybeT.hs {-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-} ~~~ 是花些時間來寫這些樣板實例呢,還是顯式地使用 lift 呢?這取決于這個monad變換器的用途。 如果我們只在幾種有限的情況下使用它,那么只提供 MonadTrans 實例就夠了。在這種情況下,也無妨提供一些依然有意義的實例,比如 MonadIO。另一方面,若我們需要在大量的情況下使用這一monad變換器,那么花些時間來完成這些實例或許也不錯。 ## 以Monad棧替代Parse類型 現在我們已開發了一個支持提早退出的monad變換器,可以用其來輔助開發了。例如,此處若想處理解析一半失敗的情況,便可以用這一以我們的需求定制的monad變換器來替代我們在第十章“隱式狀態”一節開發的 Parse 類型。 ## 練習 1. 我們的Parse monad還不是之前版本的完美替代。因為其用的是 Maybe 而不是 Either 來代表結果。因此在失敗時暫時無法提供任何有用的信息。 > 構建一個 EitherTs (其中 s 是某個類型)來表示結果,并用其實現更適合的 Parse monad以在解析失敗時匯報具體錯誤信息。 或許在你探索Haskell庫的途中,在 Control.Monad.Error 遇到過一個 Either 類型的 Monad 實例。我們建議不要參照它來完成你的實現,因為它的設計太局限了:雖然其將 EitherString 變成一個monad,但實際上把 Either 的第一個類型參數限定為 String 并非必要。 提示: 若你按照這條建議來做,你的定義中或許需要使用 FlexibleInstances 語言擴展。 ## 注意變換器堆疊順序 從早先使用 ReaderT 和 StateT 的例子中,你或許會認為疊加monad變換器的順序并不重要。事實并非如此,考慮在 State 上疊加 StateT 的情況,或許會助于你更清晰地意識到:堆疊的順序確實產生了結果上的區別:類型 StateTInt(StateString) 和類型 StateTString(StateInt) 或許攜帶的信息相同,但它們卻無法互換使用。疊加的順序決定了我們是否要用 lift 來取得狀態中的某個部分。 下面的例子更加顯著地闡明了順序的重要性。假設有個可能失敗的計算,而我們想記錄下在什么情況下其會失敗: ~~~ -- file: ch18/MTComposition.hs {-# LANGUAGE FlexibleContexts #-} import Control.Monad.Writer import MaybeT problem :: MonadWriter [String] m => m () problem = do tell ["this is where i fail"] fail "oops" ~~~ 那么這兩個monad棧中的哪一個會帶給我們需要的信息呢? ~~~ type A = WriterT [String] Maybe type B = MaybeT (Writer [String]) a :: A () a = problem b :: B () b = problem ~~~ 我們在 ghci 中試試看: ~~~ ghci> runWriterT a Loading package mtl-1.1.0.1 ... linking ... done. Nothing ghci> runWriter $ runMaybeT b (Nothing,["this is where i fail"]) ~~~ 看看執行函數的類型簽名,其實結果并不意外: ~~~ ghci> :t runWriterT runWriterT :: WriterT w m a -> m (a, w) ghci> :t runWriter . runMaybeT runWriter . runMaybeT :: MaybeT (Writer w) a -> (Maybe a, w) ~~~ 在 Maybe 上疊加 WriterT 的策略使 Maybe 成為下層monad,因此 runWriterT 必須給我們以 Maybe 為類型的結果。在測試樣例中,我們只會在不出現任何失敗的情況下才能獲得日志! 疊加monad變換器類似于組合函數:如果我們改變函數應用的順序,那么我們并不會對得到不同的結果感到意外。同樣的道理也適用于對monad變換器的疊加。 ## 縱觀Monad與Monad變換器 本節,讓我們暫別細節,討論一下用monad和monad變換器編程的優缺點。 ## 對純代碼的干涉 在實際編程中,使用monad的最惱人之處或許在于其阻礙了我們使用純代碼。很多實用的純函數需要一個monad版的類似函數,而其monad版只是加上一個占位參數 m 供monad類型構造器填充: ~~~ ghci> :t filter filter :: (a -> Bool) -> [a] -> [a] ghci> :i filterM filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a] -- Defined in Control.Monad ~~~ 然而,這種覆蓋是有限的:標準庫中并不總能提供純函數的monad版本。 其中有一部分歷史原因:Eugenio Moggi于1988年引入了使用monad編程的思想。而當時Haskell 1.0標準尚在開發中。現今版本的 Prelude 中的大部分函數可以追溯到于1990發布的Haskell 1.0。在1991年,Philip Wadler開始為更多的函數式編程聽眾作文,闡述monad的潛力。從那時起,monad開始用于實踐。 直到1996年Haskell 1.3標準發布之時,monad才得到了支持。但是在那時,語言的設計者已經受制于維護向前兼容性: 它們無法改變 Prelude 中的函數簽名,因為那會破壞現有的代碼。 從那以后,Haskell社區學會了很多合適的抽象。因此我們可以寫出不受這一純函數/monad函數分裂影響的代碼。你可以在 Data.Traversable 和 Data.Foldable 中找到這些思想的精華。 盡管它們極具吸引力,由于版面的限制。我們不會在本書中涵蓋相關內容。但如果你能輕易理解本章內容,自行理解它們也不會有問題。 在理想世界里,我們是否會與過去斷絕,并讓 Prelude 包含 Traversable 和 Foldable 類型呢?或許不會,因為學習Haskell本身對新手來說已經是個相當刺激的歷程了。在我們已經了解functor和monad之后, Foldable 和 Traversable 的抽象是十分容易理解的。但是對學習者來說這意味著擺在他們面前的是更多純粹的抽象。若以教授語言為目的, map 操作的最好是列表,而不是functor。 [譯注:實際上,自GHC 7.10開始, Foldable 和 Traversable 已經進入了 Prelude 。一些函數的類型簽名會變得更加抽象(以GHC 7.10.1為例): ~~~ ghci-7.10.1> :t mapM mapM :: (Monad m, Traversable t) => (a -> m b) -> t a -> m (t b) ghci-7.10.1> :t foldl foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b ~~~ 這并不是一個對初學者友好的改動,但由于新的函數只是舊有函數的推廣形式,使用舊的函數簽名依舊可以通過類型檢查: ~~~ ghci-7.10.1> :t (mapM :: Monad m => (a -> m b) -> [a] -> m [b]) (mapM :: Monad m => (a -> m b) -> [a] -> m [b]) :: Monad m => (a -> m b) -> [a] -> m [b] ghci-7.10.1> :t (foldl :: (b -> a -> b) -> b -> [a] -> b) (foldl :: (b -> a -> b) -> b -> [a] -> b) :: (b -> a -> b) -> b -> [a] -> b ~~~ 若在學習過程中遇到障礙,不妨暫且以舊的類型簽名來理解它們。] ## 對次序的過度限定 我們使用monad的一個基本原因是:其允許我們指定效果發生的次序。再看看我們早先寫的一小段代碼: ~~~ -- file: ch18/MTComposition.hs {-# LANGUAGE FlexibleContexts #-} import Control.Monad.Writer import MaybeT problem :: MonadWriter [String] m => m () problem = do tell ["this is where i fail"] fail "oops" ~~~ 因為我們在monad中執行, tell 的效果可以保證發生在 fail 之前。這里的問題在于,這個次序并不必要,但是我們卻得到了這樣的次序保證。編譯器無法任意安排monad式代碼的次序,即便這么做能使代碼效率更高。 [譯注:解釋一下這里的“次序并不必要”。回顧之前對疊加次序問題的討論: ~~~ type A = WriterT [String] Maybe type B = MaybeT (Writer [String]) a :: A () a = problem -- runWriterT a == Nothing b :: B () b = problem -- runWriter (runMaybeT b) == (Nothing, ["this is where i fail"]) ~~~ 下面把注意力集中于 a : 注意到 runWriterTa==Nothing , tell 的結果并不需要,因為接下來的 fail 取消了計算,將之前的結果拋棄了。利用這個事實,可以得知讓 fail 先執行效率更高。同時注意對 fail 和 tell 的實際處理來自monad棧的不同層,所以在一定限制下調換某些操作的順序會不影響結果。但是由于這個monad棧本身也要是個monad,使這種本來可以進行的交換變得不可能了。] ## 運行時開銷 最后,當我們使用monad和monad變換器時,需要付出一些效率的代價。 例如 State monad攜帶狀態并將其放在一個閉包中。在Haskell的實現中,閉包的開銷或許廉價但絕非免費。 Monad變換器把其自身的開銷附加在了其下層monad之上。每次我們使用 (>>=) 時,MaybeT變換器便需要包裝和解包。而由 ReaderT , StateT 和 MaybeT 依次疊加組成的monad棧,在每次使用 (>>=) 時,更是有一系列的簿記工作需要完成。 一個足夠聰明的編譯器或許可以將這些開銷部分,甚至于全部消除。但是那種深度的復雜工作尚未廣泛適用。 但是依舊有些相對簡單技術可以避免其中的一些開銷,版面的限制只允許我們在此做簡單描述。例如,在continuation monad中,對 (>>=) 頻繁的包裝和解包可以避免,僅留下執行效果的開銷。所幸的是使用這種方法所要考慮的大部分復雜問題,已經在函數庫中得到了處理。 這一部分的工作在本書寫作時尚在積極的開發中。如果你想讓你對monad變換器的使用更加高效,我們推薦在Hackage中尋找相關的庫或是在郵件列表或IRC上尋求指引。 ## 缺乏靈活性的接口 若我們只把 mtl 當作黑盒,那么所有的組件將很好地合作。但是若我們開始開發自己的monad和monad變換器,并想讓它們于 mtl 提供的組件配合,這種缺陷便顯現出來了。 例如,我們開發一個新的monad變換器 FooT ,并想沿用 mtl 中的模式。我們就必須實現一個類型類 MonadFoo 。若我們想讓其更好地和 mtl 配合,那么便需要提供大量的實例來支持 mtl 中的類型類。 除此之外,還需要為每個 mtl 中的變換器提供 MonadFoo 的實例。大部分的實例實現幾乎是完全一樣的,寫起來也十分乏味。若我們想在 mtl 中集成更多的monad變換器,那么我們需要處理的各類活動部件將達到引入的monad變換器數量的 *平方級別* ! 公平地看來,這個問題會只影響到少數人。大部分 mtl 的用戶并不需要開發新的monad。 造成這一 mtl 設計缺陷的原因在于,它是第一個monad變換器的函數庫。想像其設計者投入這個未知的領域,完成了大量的工作以使這個強大的函數庫對于大部分用戶來說做到簡便易用。 一個新的關于monad和變換器的函數庫 monadLib ,修正了 mtl 中大量的設計缺陷。若在未來你成為了一個monad變換器的中堅駭客,這值得你一試。 平方級別的實例定義實際上是使用monad變換器帶來的問題。除此之外另有其他的手段來組合利用monad。雖然那些手段可以避免這類問題,但是它們對最終用戶而言仍不及monad變換器便利。幸運的是,并沒有太多基礎而泛用的monad變換器需要去定義實現。 ## 綜述 Monad在任何意義下都不是處理效果和類型的終極途徑。它只是在我們探索至今,處理這類問題最為實用的技術。語言的研究者們一直致力于找到可以揚長避短的替代系統。 盡管在使用它們時我們必須做出妥協,monad和monad變換器依舊提供了一定程度上的靈活度和控制,而這在指令式語言中并無先例。 僅僅幾個聲明,我們就可以給分號般基礎的東西賦予嶄新的意義。 [譯注:此處的分號應該指的是 do 標記中使用的分號。]
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看