改进冗长的Haskell Monadic代码

Ral*_*lph 1 monads haskell composition yesod

以下函数是Yesod REST服务器的一部分,通过电子邮件地址在MongoDB数据库中搜索现有用户,并返回Maybe User:

{-# LANGUAGE DeriveGeneric #-}

module Model.User where

import           Database.MongoDB (Action, findOne, select, (=:))
import qualified Database.MongoDB as M
import           GHC.Generics     (Generic)
import           Import

data User = User
  { userEmail     :: Text
  , userFirstName :: Text
  , userLastName  :: Text
  } deriving (Generic, Show)

collection :: Text
collection = "users"

instance FromJSON User
instance ToJSON User

findForEmail :: Text -> Action IO (Maybe User)
findForEmail email = do
  maybeDocument <- findOne (select [ "email" =: email ] collection)
  case maybeDocument of
    Just document -> do
      email' <- M.lookup "email" document
      firstName <- M.lookup "firstName" document
      lastName <- M.lookup "lastName" document
      return $ Just $ User email' firstName lastName
    Nothing       -> return Nothing
Run Code Online (Sandbox Code Playgroud)

涉及两个"嵌套"monad(maybeDocument <-)的部分感觉非常"啰嗦".findOne返回a Maybe Documentlookup返回a Maybe v.

这可以缩短,也许使用申请人?

UPDATE

我把它简化为:

maybeDocument <- findOne (select [ "email" =: email ] collection)
case maybeDocument of
  Just document ->
    return $ User <$> M.lookup "email" document
                  <*> M.lookup "firstName" document
                  <*> M.lookup "lastName" document
  Nothing       -> return Nothing
Run Code Online (Sandbox Code Playgroud)

但它仍然感觉很沉重.有没有办法把maybeDocument <-monad和lookups 结合起来?

lef*_*out 6

case在a上Maybe,Just映射到另一个MaybeNothing立即到Nothing,与使用一个monadic绑定相同.(当然你需要保持return出局,这里的行为是错误的monad.)

   maybeDocument <- findOne (select [ "email" =: email ] collection)
   return $ maybeDocument >>= \document ->
             User <$> M.lookup "email" document
                  <*> M.lookup "firstName" document
                  <*> M.lookup "lastName" document
Run Code Online (Sandbox Code Playgroud)

此外,maybeDocument变量有点尴尬,我们可以消除这一点:请注意,因为结果只是return编入Action IOmonad,你根本不需要一个do块:你只是 - Functor覆盖结果!这可以很好地完成无点:

   fmap (>>= \document ->
             User <$> M.lookup "email" document
                  <*> M.lookup "firstName" document
                  <*> M.lookup "lastName" document
        ) $ findOne (select [ "email" =: email ] collection)
Run Code Online (Sandbox Code Playgroud)

如果我们能够保留原始的"评估顺序",你可能会认为这看起来会更好一些.我们可以使用(非标准)反向应用运算符

      findOne (select [ "email" =: email ] collection)  <&>  (>>=
         \document -> User <$> M.lookup "email" document
                           <*> M.lookup "firstName" document
                           <*> M.lookup "lastName" document  )
Run Code Online (Sandbox Code Playgroud)

当然,这使得很难掌握每个操作员正在做什么,但我认为IMO在这种简洁的代码中的总体意图非常明确.