2013-04-17 68 views
14

這是一個與API設計實踐有關的問題,用於爲Haskell庫定義您自己的Monad實例。定義Monad實例似乎是隔離DSL的一個好方法,例如monad-par,hdph中的monad:ParProcess分佈式進程; Eval並行等...何時(以及何時不定義)定義Monad

我拿兩個haskell庫的例子,其目的是IO與數據庫後端。我使用的示例爲Riak IO的riak,Redis IO的hedis

在hedis中,一個Redis monad is defined。從那裏,你Redis的身份運行IO:

data Redis a -- instance Monad Redis 
runRedis :: Connection -> Redis a -> IO a 
class Monad m => MonadRedis m 
class MonadRedis m => RedisCtx m f | m -> f 
set :: RedisCtx m f => ByteString -> ByteString -> m (f Status) 

example = do 
    conn <- connect defaultConnectInfo 
    runRedis conn $ do 
    set "hello" "world" 
    world <- get "hello" 
    liftIO $ print world 

在了Riak,情況就不同了:

create :: Client -> Int -> NominalDiffTime -> Int -> IO Pool 
ping :: Connection -> IO() 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

example = do 
    conn <- connect defaultClient 
    ping conn 

runRedis文檔說:「runRedis的每次調用需要從連接的網絡連接並且運行給定的Redis動作,因此調用runRedis可能會阻止來自池中的所有連接正在使用。「。然而,riak包也實現連接池。這一方案無需額外的單子情況下完成的IO單子的頂部:

create :: Client-> Int -> NominalDiffTime -> Int -> IO Pool 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

exampleWithPool = do 
    pool <- create defaultClient 1 0.5 1 
    withConnection pool $ \conn -> ping conn 

所以,這兩個包之間的類比可以歸結爲這兩個功能:

runRedis  :: Connection -> Redis a -> IO a 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

至於我可以告訴大家, hedis軟件包引入了一個monad Redis來使用runRedis用redis封裝IO操作。相反,withConnection中的riak包只需要一個採用Connection的函數,並在IO monad中執行該函數。

那麼,定義自己的Monad實例和Monad堆棧的動機是什麼?爲什麼riak和redis包在解決這個問題上存在差異?

+6

作爲答案的上下文 - 如果不明顯,「Redis a」和「Connection - > IO a」類型大致相同。所以這實質上是一種表面上的區別,與'env - > IO a'和''ReaderT env IO a''相比。 –

+0

然後,這意味着也許這兩個都是不正確的,'Codensity IO Connection'是他一直想要的monad。 –

回答

10

對我來說,這完全是關於封裝和保護用戶以防未來的實施變化。正如凱西指出的那樣,這兩者現在大致相當 - 基本上是一個Reader Connection單子。但想象一下,這些行爲將如何面對不確定的變化。如果這兩個軟件包最終決定用戶需要一個狀態monad接口而不是讀卡器呢?如果出現這種情況,了Riak的withConnection功能將改變這樣的類型簽名:

withConnection :: Pool -> (Connection -> IO (a, Connection)) -> IO a 

這將要求用戶代碼徹底改變。但Redis軟件包可以在不破壞用戶的情況下實現這種改變。

現在,人們可能會爭辯說,這種假設情景是非常不現實的,而不是您需要計劃的事情。在這兩種特殊情況下,情況可能如此。但是所有項目都會隨着時間的推移而變化,並且經常以不可預知的方式進行。定義您自己的monad允許您隱藏用戶的內部實現細節,並通過未來更改提供更穩定的接口。

這樣說的時候,有人可能會得出結論,定義你自己的monad是最好的方法。但我不認爲情況總是如此。 (lens庫是一個潛在的好例子。)定義一個新的monad會帶來成本。如果您使用monad變壓器,可能會導致性能損失。在其他情況下,API可能最終變得更加冗長。 Haskell非常好,讓你保持語法非常簡單,在這種情況下,差異不是很大 - 可能有幾個liftIO用於redis和幾個lambda用於riak。

軟件設計很少被切割和乾燥。很少有人能夠自信地說出何時何地不定義自己的monad。但是我們可以意識到所涉及的權衡,以幫助我們評估我們遇到的各種情況。

1

在這種情況下,我認爲實現monad是一個錯誤。它只是爲了讓它們實現各種設計模式而感到焦慮的java開發人員。

hdbc例如也在普通的IO monad中工作。

Monad for redis庫不會帶來任何用處。它實現的唯一的事情是擺脫一個函數參數(連接)。但是你支付它在redis monad中解除每個IO操作。

此外,如果你在任何時候需要用2個的Redis數據庫的工作,現在你就要有一個困難時期試圖找出解除其操作,其中:)

實現一個單子的唯一原因是創建一個新的DSL 。正如你所看到的hedis沒有創建一個新的DSL。它的操作與任何其他數據庫庫完全相同。因此,hedis中的monad是膚淺的,沒有道理。