使用Maybe a、IO a和MaybeT IO a进行操作

4
我正在编写一个提示 - 响应式系统,其中包含各种组合的Maybe a,IO a 和 MaybeT IO a。需要考虑许多问题。有些IO操作没有无效输入(因此没有包装在MaybeT中),有些则是(并返回MaybeT IO a);有些不是IO操作,但可能失败,所以返回Maybe a;还有一些只是普通值。为了使所有内容正确,我必须记住众多的组合:<$>、Just、fmap、MaybeT、lift、=<<以及return。有没有更简单的方法来管理或推断需要使用哪些函数将我的值放置到需要它们的位置?或者我只能希望随着时间的推移变得更加娴熟?以下是我的示例:
getPiece :: Player -> Board -> MaybeT IO Piece
getPiece player@(Player pieces _ _ _) board = piece
    where
        promptString = displayToUserForPlayer player board ++ "\n" ++ (display player) ++ "\n" ++ "Enter piece number: "
        input :: MaybeT IO String
        input = lift $ prompt promptString
        index :: MaybeT IO Int
        index = MaybeT <$> return <$> ((fmap cvtFrom1indexedInt) . maybeRead) =<< input
        piece :: MaybeT IO Piece
        piece = MaybeT <$> return <$> maybeIndex pieces =<< index

getRotatedPiece :: Player -> Board -> MaybeT IO Piece
getRotatedPiece player@(Player pieces _ _ _) board = piece
    where
        promptString :: MaybeT IO String
        promptString = (++) <$> displayListString <*> restOfString
        input :: MaybeT IO String
        input = MaybeT <$> (fmap Just) <$> prompt =<< promptString
        index :: MaybeT IO Int
        index = MaybeT <$> return <$> ((fmap cvtFrom1indexedInt) . maybeRead) =<< input
        piece :: MaybeT IO Piece
        piece = MaybeT <$> return <$> maybeIndex pieces =<< index
        rotatedPieceList :: MaybeT IO [Piece]
        rotatedPieceList = rotations <$> getPiece player board
        displayListString :: MaybeT IO String
        displayListString = displayNumberedList <$> rotatedPieceList
        restOfString :: MaybeT IO String
        restOfString = MaybeT <$> return <$> Just $ "\nEnter rotation number:"

我必须说,我对缺乏简洁性感到失望,即使我去掉类型提示,在C#或Python中完成相同的操作可能会写一个更短的函数。


3
使用lift,尝试编写所有单子代码,而不使用构造函数(这将解决您的冗长问题,并使重构更容易) 。 - jberryman
2个回答

18

由于您提供的只是代码片段,我无法尝试重构它。然而,以下是我的建议:大多数单子都有对应的类型类。原因正是您在这里所需的:当您使用单子变换器创建一个单子时,它将继承内部单子的操作(如果适用)。因此,您可以忘记内部单子,只需在最终单子中工作。

在您的情况下,您有 MaybeT IO。它是 MonadPlusMonadIO 的实例。因此,您可以重构返回 Maybe something 的代码,使其与通用的 MonadPlus 实例一起工作,只需用 return 替换 Just,用 mzero 替换 Nothing。像这样:

-- before
checkNumber :: Int -> Maybe Int
checkNumber x | x > 0       = Just x
              | otherwise   = Nothing x
-- after
checkNumber :: MonadPlus m => Int -> m Int
checkNumber x | x > 0       = return x
              | otherwise   = mzero
-- or just: checkNumber = mfilter (> 0) . return
它将与任何MonadPlus一起工作,包括MaybeMaybeT IO
您可以重构返回IO something的代码以使用通用的MonadIO实例:
-- before
doSomeIO :: IO ()
doSomeIO = getLine >>= putStrLn
-- after
doSomeIO :: MonadIO m => m ()
doSomeIO = liftIO $ getLine >>= putStrLn

这样,你就可以忘掉像<$>/fmap/liftMJustMaybeT等等。你只需要使用returnmzero,而在某些情况下使用liftIO

这还将帮助你创建更通用的代码。如果你后来意识到需要向monad堆栈中添加东西,只要新的monad堆栈实现了相同的类型类,现有的代码就不会出问题。


0
我给出一个不那么雄心勃勃的答案。看着你的代码,像 getPiece 这样的操作实际上没有从特定错误位置返回任何信息。如果你真的想要这些信息,你可以尝试只使用IO,并将异常转换为 Maybe 值来解决问题。以下是一些示例代码,其中引用了你的代码中一些未定义的函数:
import Control.Exception (handle, IOException)

data Board = Board deriving (Show)
data Piece = Piece deriving (Show)
type Pieces = [Piece]
data Player = Player Pieces () () () deriving (Show)

prompt :: String -> IO String
prompt = undefined

cvtFrom1indexedInt :: Int -> Int
cvtFrom1indexedInt = undefined

maybeIndex :: Pieces -> Int -> Maybe Piece
maybeIndex = undefined

displayToUserForPlayer :: Player -> Board -> String
displayToUserForPlayer = undefined

display :: Player -> String
display = undefined

-- I used this when testing, to deal with the Prelude.undefined errors
--returnSilently :: SomeException -> IO (Maybe a)
returnSilently :: IOException -> IO (Maybe a)
returnSilently e = return Nothing

getPiece :: Player -> Board -> IO (Maybe Piece)
getPiece player@(Player pieces _ _ _) board = handle returnSilently $ do
    let promptString = displayToUserForPlayer player board ++ "\n" ++ (display player) ++ "\n" ++ "Enter piece number: "
    input <- prompt promptString
    let index = cvtFrom1indexedInt (read input)
    return (maybeIndex pieces index)

main = do
    maybePiece <- getPiece (Player [] () () ()) Board
    putStrLn ("Got piece: " ++ show maybePiece)

值得注意的是,我已经从MaybeT IO Piece转移到了IO (Maybe Piece)。我没有使用fmaplift,而是使用do符号引用了我的IO操作的中间结果。
针对你关于C#或Python的评论,我希望这是你正在寻找的更简单的答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接