2016-02-24 88 views
1

我想在hedis(redis lib)的基礎上編寫一個簡單的DSL。我們的目標是編寫類似的功能:在hedis上構建monad,haskell redis lib

iWantThis :: ByteString -> MyRedis() 
iWantThis bs = do 
    load bs -- :: MyRedis() It fetches a BS from Redis and puts it as 
      -- the state in a state monad 
    bs <- get -- :: MyRedis (Maybe ByteString) Gets the current state 
    put $ doSomethingPure bs -- :: MyMonad() Updates the state 
    commit -- :: MyRedis() Write to redis 

的基本思想是從Redis的獲取數據,把它放在一個狀態單子,做一些東西與狀態,然後把更新的狀態回Redis的。

顯然,它應該是原子的,所以loadput應該發生在同一個Redis事務中。 Hedis允許通過將電話Redis打包爲RedisTx (Queued a)。例如,我們有get :: ByteString -> RedisTx (Queued a)Queued是一個monad,然後在Queued a上運行multiExec,在同一個事務中執行Queued a中的所有內容。於是,我就確定我MyRedis這樣:

import qualified Database.Redis as R 
newtype MyRedis a = MyRedis { runMyRedis :: StateT MyState R.RedisTx a } -- deriving MonadState, MyState... 

run函數調用multiExec所以我相信,只要我留在MyRedis一切都發生在同一個事務。

run :: MyRedis (R.Queued a) -> MyState -> IO (R.TxResult a) 
run m s = R.runRedis (undefined :: R.Connection) (R.multiExec r) 
    where r = evalStateT (runMyRedis m) s 

而且,我可以定義爲commit

commit :: ByteString -> MyRedis (R.Queued R.Status) 
commit bs = do 
    MyState new <- get 
    (MyRedis . lift) (R.set bs new) 

而一個computation會是什麼樣子:

computation :: MyRedis (R.Queued R.Status) 
computation = do 
    load gid 
    MyState bs <- get 
    put $ MyState (reverse bs) 
    commit gid 
    where gid = "123" 

但我無法弄清楚如何寫 「負荷」

load :: ByteString -> MyRedis() 
load gid = undefined 

實際上,我認爲不可能寫load,因爲getByteString -> RedisTx (Queued (Maybe ByteString))類型,我沒有辦法在不執行它的情況下窺探Queued monad。

問題:

  1. 是不是正確的,因爲HEDIS的get類型的,它沒有意義的定義與上述語義的load功能?

  2. 是否可以更改MyRedis類型定義使其工作?

  3. Hedis沒有定義一個RedisT monad變壓器。如果這樣的變壓器存在,會有幫助嗎?

  4. Hedis定義(但不會導出到lib用戶)MonadRedis typeclass;會讓我的monad成爲類型類的幫助實例嗎?

  5. 這是正確的做法嗎?我想:

    • 摘要在Redis的(我可能有一天會切換到另一個DB)
    • 限制提供給我的用戶Redis的功能(基本上只提升到MyRedisgetset
    • 保證,當我運行我的單子都在相同的(Redis的)發生交易
    • 把我Redis的抽象在同級別其它功能在我的單子

你可以玩http://pastebin.com/MRqMCr9Q的代碼。對不起pastebin,目前lpaste.net已經關閉。

+0

「我沒有辦法窺探排隊的monad而不執行它。」真?那麼'>> = :: Queued a - >(a - > Queued b) - > Queued?你已經給了'計算:: MyRedis(R.Queued R.Status)' - 非常明智的類型。但'load'應該是'load :: ByteString - > MyRedis(Queued())'。瀏覽圖書館文檔,似乎「排隊」和「RedisTx」在某種程度上是內在聯繫在一起的..我不明白爲什麼RedixTx每個人都能夠在沒有排隊的情況下出現(在多態函數中,當然,但這些只會是用Queued實例化) – user2407038

+0

另外,這個問題非常非常廣泛。這顯然是一個問題!特別是「這是正確的做法嗎?」 - 這幾乎不可能回答。如何量化「正確」方法是什麼?實際上,我認爲,大多數事情都不是「正確的」 - 只是「足夠好」。你幾乎可以肯定地完成你陳述的目標,真正的問題是,redis的設計決策(和Haskell綁定它的)會幫助還是阻礙你?但這自然是一個非常難以回答的先驗問題...... – user2407038

+0

「我沒有辦法窺視排隊的monad而不執行它。」 Redis庫以這種方式設計的原因是爲了防止以後的'RedisTx'行動取決於早期的結果。在你處理事務時,早先的命令還沒有運行。你想要什麼都做不到。現在,如果是我,我可能會試圖通過暴露批處理命令的'Applicative'接口來使這一點更加明顯,但我希望lib的作者有一個很好的理由這樣做。 –

回答

2

你想要什麼是不可能的。特別是,您不能在一個Redis事務中運行計算的時候提供一個monadic接口。與您使用的圖書館無關 - 這不僅僅是Redis能夠做到的。

Redis事務與您可能從關係數據庫領域習慣使用的ACID事務有很大不同。 Redis事務處理有批處理語義,這意味着後面的命令不能以任何方式依賴於早期命令的結果。

Look:這裏的例子與您的示例類似,在Redis命令行上運行。

> set "foo" "bar" 
OK 
> multi 
OK 
> get "foo" 
QUEUED -- I can't now set "baz" to the result of this command because there is no result! 
> exec 
1) "bar" -- I only get the result after running the whole tran 

無論如何,這是該庫的有點奇怪Queued類型的目的:這個想法是阻止你訪問任何一個批處理命令的結果,直到該批次結束。 (看起來作者想要抽象化批量和非批量的命令,但有更簡單的方法可以做到這一點,請參閱下面我將簡化事務界面)。下一步「涉及Redis交易時,但(>>=) :: m a -> (a -> m b) -> m b的整點是後面的效果可能取決於先前的結果。你必須選擇monads和交易。


如果你決定要交易,有以Monad另一種稱爲Applicative這handlily支持純靜態效果。這正是我們需要的。這裏有一些(完全未經測試的)代碼,說明我如何烹調你想法的Applicative版本。

newtype RedisBatch a = RedisBatch (R.RedisTx (R.Queued a)) 
-- being a transactional batch of commands to send to redis 

instance Functor RedisBatch where 
    fmap = liftA 

instance Applicative RedisBatch where 
    pure x = RedisBatch (pure (pure x)) 
    (RedisBatch rf) <*> (RedisBatch rx) = RedisBatch $ (<*>) <$> rf <*> rx 

-- no monad instance 

get :: ByteString -> RedisBatch (Maybe ByteString) 
get key = RedisBatch $ get key 

set :: ByteString -> ByteString -> RedisBatch (R.Status) 
set key val = RedisBatch $ set key val 

runBatch :: R.Connection -> RedisBatch a -> IO (R.TxResult a) 
runBatch conn (RedisBatch x) = R.runRedis conn (R.multiExec x) 

如果我想在抽象事務有或不行爲,因爲圖書館筆者試圖這樣做,我會寫第二類RedisCmd露出單子接口,以及包含原始的我的一類操作,實例爲我的兩個RedisBatchRedisCmd類型。

class Redis f where 
    get :: ByteString -> f (Maybe ByteString) 
    set :: ByteString -> ByteString -> f (R.Status) 

現在,有型的(Applicative f, Redis f) => ...計算可以連續工作任一行爲(交易與否),而是那些需要一個單子(Monad m, Redis m) => ...只能夠在非事務性模式下運行。


當所有的說法和完成後,我不相信它是值得的。人們似乎喜歡在像這樣的庫上構建抽象概念,總是提供比圖書館更少的功能,並且編寫更多的代碼來隱藏bug。每當有人說「我可能想要切換數據庫」時,我感嘆:這是唯一足夠抽象的抽象目的是不提供功能的。擔心在需要時(即從不)切換數據庫。另一方面,如果你的目標不是爲了抽象數據庫,而是爲了清理界面,最好的辦法可能是分叉庫。

+0

感謝您的冗長答覆。的確,我沒有正確理解Redis的交易模式。完全同意你和我需要一個應用界面的事實。至於這一切的價值,我做了那麼一點,因爲我想通過redis進行抽象,但主要是爲了濫用類型系統的樂趣。 – cpa