2013-10-24 56 views
2

我正在開發使用Scotty和Persistent的REST後端,我找不出正確的方式來處理錯誤。透明的錯誤處理

我有幾個函數來訪問數據庫,如:

getItem :: Text -> SqlPersistM (Either Error Item) 

它的SQL單子內返回無論是。然後,我用它在我的行動來獲取項目,並返回它的JSON表示:

get "/items/:name" $ do 
    name  <- param "name" 
    eitherItem <- lift $ MyDB.getItem name 

    case eitherItem of 

     Left NotFound -> do 
     status status404 
     json NotFound 

     Left InvalidArgument -> do 
     status status400 
     json BadRequest 

     Right item -> json item 

我可以讓代碼通過引入一些助手漂亮,但該模式將保持不變 - Access數據庫,檢查錯誤,提供適當的迴應。

我想擺脫錯誤的,我的行爲完全處理:

get "/items/:name" $ do 
    name <- param "name" 
    item <- lift $ MyDB.getItem name 

    -- In case of error, appropriate 
    -- HTTP response will be sent, 
    -- else just continue 

    bars <- lift $ MyDB.listBars 

    -- In case of error, appropriate 
    -- HTTP response will be sent, 
    -- else just continue 

    json (process item bars) 

getItem可能會返回錯誤,並且它會以某種方式轉換爲json響應,全都對操作代碼透明。如果getItem對action和json響應一無所知,那將會很好。

我在過去使用命令式語言解決了這個問題,它通過從各處引發異常,然後將它捕獲到一個地方並呈現適當的響應。我想Haskell也是可以的,但我想知道如何使用功能工具來解決這個問題。

我知道monads可能會短路(如Either >> Either >> Either),但不知道如何在這個稍微複雜的情況下使用它。

+0

我不認爲'json [item,bars]'甚至會編譯。列表類型是同類的。 – Ankur

+0

是的,我的錯。將修復例子。 – lambdas

回答

4

的解決方案是使用EitherT monad變壓器(來自either包)來處理短路的錯誤。 EitherT使用與命令式語言中的檢查異常完全一樣的功能來擴展任何monad。

這適用於任何「基地」的單子,m,讓我們假設你有兩種類型的計算,其中有些失敗了,有些則永遠不會失敗:

fails :: m (Either Error r) -- A computation that fails 
succeeds :: m r     -- A computation that never fails 

然後,您可以解除兩者的這些計算到了monod的EitherT Error m。你解除故障計算的方法是將它們包裝在EitherT構造函數(構造函數具有相同的名稱類型):

EitherT :: m (Either Error r) -> EitherT Error m r 

EitherT fails :: EitherT Error m r 

通知的Error類型現在是如何吸收到單子並沒有顯示出來在返回值中。

解除了成功的計算,使用lift,從transformers

lift :: m r -> EitherT Error m r 

lift succeeds :: EitherT Error m r 

類型的lift實際上是更普遍的,因爲它可以用於任何單子轉換。它的一般類型是:

lift :: (MonadTrans t) => m r -> t m r 

...這裏對我們來說tEitherT Error

同時使用這些技巧,那麼你可以將你的代碼在錯誤自動短路:

import Control.Monad.Trans.Either 

get "/items/:name" $ do 
    eitherItem <- runEitherT $ do 
     name <- lift $ param "name" 
     item <- EitherT $ lift $ MyDB.getItem name 
     bars <- EitherT $ lift $ MyDB.listBars 
     lift $ json (process item bars) 
    case eitherItem of 
     Left NotFound -> do 
      status status404 
      json NotFound 
     Left InvalidArgument -> do 
      status status400 
      json BadRequest 
     Right() -> return() 

runEitherT運行您EitherT,直到它完成或它遇到的第一個錯誤。如果計算失敗,則eitherItemrunEitherT返回將是Left,如果計算成功,將返回Right

這使您可以在塊之後將錯誤處理壓縮爲單個case語句。

如果您從我的errors包中提供的Control.Error導入catch,您甚至可以執行類似catch的行爲。這讓你寫的代碼非常相似,勢在必行代碼:

(do 
    someEitherTComputation 
    more stuff 
) `catch` (\eitherItem -> do 
    handlerLogic 
    more stuff 
) 

但是,你仍然需要在某一時刻使用runEitherT在代碼解開了EitherT當你做了,即使你捕獲並處理錯誤。這就是爲什麼這個簡單的例子,我建議直接使用runEitherT而不是catch

+0

這是一個比我的同樣的事情更好,更正確的解釋。 – Cirdec

+0

@cirdec謝謝! :) –

+0

@GabrielGonzalez謝謝你,正是我需要的。我剛剛注意到'SqlPersistT'是'MonadThrow'的一個實例,使用'monadThrow'代替它會更容易嗎(如果可能的話)? – lambdas

1

你需要的是像下面的函數,它可以映射ErrorActionM

handleError :: Error -> ActionM() 
handleError NotFound = status status404 >> json NotFound 
handleError InvalidArgument = status status400 >> json BadRequest 
...other error cases... 

respond :: ToJSON a => Either Error a -> ActionM() 
respond (Left e) = handleError e 
respond (Right item) = json item 

然後在你的處理函數使用上述功能:

get "/items/:name" $ do 
    name  <- param "name" 
    eitherItem <- lift $ MyDB.getItem name 
    respond eitherItem 
+0

這就是我的意思是「介紹助手」。也許我過於簡單化了,對不起。通常我想在返回之前轉換'item'或/並鏈接幾個db查詢。 – lambdas

2

您在查找Error monad

你想寫類似:

get "/items/:name" $ handleErrorsInJson do 
    name <- param "name" 
    item <- lift $ MyDB.getItem name  
    bars <- lift $ MyDB.listBars  
    json (process item bars) 

transformers' ErrorT增加處理到現有的單子錯誤。

要做到這一點,你需要讓你的數據訪問方法表明,他們在錯誤單子而不是通過返回Either

getItem :: Text -> SqlPersistM (Either Error Item) 

或者,你可以使用類似

toErrors :: m (Either e a) -> ErrorT e m a 
遇到錯誤

使用您的現有功能而不修改它們。一個快速的Hoogling表示已經有一些類型爲m (Either e a) -> ErrorT e m a的東西,那就是構造函數ErrorT。配備這個,我們可以寫:

get "/items/:name" $ handleErrorsInJson do 
    name <- lift $ param "name" 
    item <- ErrorT $ lift $ MyDB.getItem name  
    bars <- ErrorT $ lift $ MyDB.listBars  
    lift $ json (process item bars) 

handleErrorsInJson會是什麼?從ANKUR的例子借用handleError

handleErrorsInJson :: ErrorT Error ActionM() -> ActionM() 
handleErrorsInJson = onError handleError 

onError :: (e -> m a) -> (ErrorT e m a) -> m a 
onError handler errorWrapped = do 
    errorOrItem <- runErrorT errorWrapped 
    either handler return errorOrItem 

注:我沒有檢查這對一個編譯器,有可能是在這裏的小失誤。 編輯看到加布裏埃爾的迴應後解決問題。 handleErrorsInJson不會鍵入檢查,它缺少一個急需的runErrorT

+0

非常好,謝謝! – lambdas