Работа с Maybe a, IO a и MaybeT IO a

Я пишу систему в стиле подсказки-ответа с кучей различных комбинаций Maybe a, IO a и MaybeT IO a, и есть много вещей, которые нужно учитывать. Некоторые действия ввода-вывода, для которых нет недопустимых входных данных (и, следовательно, не заключены в MaybeT), некоторые (и возвращают MaybeT IO a), некоторые не являются действиями ввода-вывода, но могут завершиться ошибкой, поэтому возвращают 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.


person Drew    schedule 06.12.2012    source источник
comment
Используйте lift и попробуйте написать весь свой монадический код без использования конструкторов (это решит вашу проблему многословия, а также упростит рефакторинг)   -  person jberryman    schedule 07.12.2012


Ответы (2)


Поскольку вы предоставили только фрагмент кода, я не могу пытаться его рефакторить. Однако я бы сделал следующее: у большинства монад есть соответствующий класс типов. Причина именно в том, что вам здесь нужно: когда вы создаете монаду с помощью преобразователя монады, она наследует операции внутренних монад (если применимо). Таким образом, вы можете забыть о внутренних монадах и работать только внутри конечной монады.

В вашем случае у вас есть MaybeT IO. Это экземпляр MonadPlus и MonadIO. Таким образом, вы можете реорганизовать код, который возвращает Maybe something, чтобы вместо этого работать с общим экземпляром MonadPlus, просто замените Just на return и Nothing на mzero. Нравиться:

-- 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, включая Maybe и MaybeT IO.

И вы можете реорганизовать код, который возвращает IO something, чтобы он работал с общим экземпляром MonadIO:

-- before
doSomeIO :: IO ()
doSomeIO = getLine >>= putStrLn
-- after
doSomeIO :: MonadIO m => m ()
doSomeIO = liftIO $ getLine >>= putStrLn

Таким образом, вы можете забыть о <$>/fmap/liftM, Just, MaybeT и т. д. Вы просто используете return, mzero и в некоторых местах liftIO.

Это также поможет вам создать более общий код. Если позже вы поймете, что вам нужно что-то добавить в стек монад, существующий код не сломается, пока новый стек монад реализует классы того же типа.

person Petr    schedule 06.12.2012

Менее амбициозный ответ от меня. Глядя на ваш код, ваши операции, такие как getPiece, на самом деле не возвращают никакой информации с конкретного сайта ошибки. Вероятно, вам может сойти с рук просто использование ввода-вывода и превращение исключений в значения 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). Вместо использования fmap или lift я только что использовал обозначение do для ссылки на промежуточные результаты моего действия IO.

Продолжая ваши комментарии о C # или Python, я надеюсь, что это был более простой ответ, который вы искали.

person monk    schedule 06.12.2012