I'm writing a prompt - response style system with a bunch of various combinations of Maybe a, IO a, and MaybeT IO a, and there is a lof of stuff to take into account. Some IO actions for which there is no invalid input (and therefore aren't wrapped in MaybeT), some which are (and return an MaybeT IO a) some which aren't IO actions but can fail, so return Maybe a, and some that are just plain values and its beginning to seem that I have to remember inordinate combinations of <$>, Just, fmap, MaybeT, lift, =<<,
and return
just to get everything to be the right type. Is there any easier way to manage this or to reason about what functions I need to use to get my values where I need them? Or do I just have to hope I get better at it with time? Here is my example:
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:"
I must say, I am disappointed at the lack of conciseness, even if I removed the type hints I could likely write a shorter function to do the same thing in C# or python
Since you provided only a code fragment, I cannot try to refactor it. However, this is what I'd do: Most monads have a corresponding type class. The reason for it is exactly what you need here: When you create a monad using a monad transformer, it will inherit the operations of the inner monads (if appropriate). So you can forget about the inner monads and work just within the final monad.
In your case, you have MaybeT IO
. It's instance of MonadPlus
and of MonadIO
. So you can refactor the code that returns Maybe something
to work with a general MonadPlus
instance instead, just replace Just
with return
and Nothing
with mzero
. Like:
-- 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
It will work with any MonadPlus
, including Maybe
and MaybeT IO
.
And you can refactor the code that returns IO something
to work with a general MonadIO
instance:
-- before
doSomeIO :: IO ()
doSomeIO = getLine >>= putStrLn
-- after
doSomeIO :: MonadIO m => m ()
doSomeIO = liftIO $ getLine >>= putStrLn
This way, you can forget about <$>
/fmap
/liftM
, Just
, MaybeT
etc. You just use return
, mzero
and in some places liftIO
.
This will also help you to create a more general code. If you later realize that you need to add something to the monad stack, the existing code won't break, as long as the new monad stack implements the same type classes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With