Skip to content

Instantly share code, notes, and snippets.

@Arkham
Created April 19, 2021 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Arkham/d028fe45105f0b3514b89d5ab7bff54f to your computer and use it in GitHub Desktop.
Save Arkham/d028fe45105f0b3514b89d5ab7bff54f to your computer and use it in GitHub Desktop.
A Monad Transformer tutorial
module MonadTrans where
import Control.Applicative (empty)
import Control.Monad (guard, join)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Identity (IdentityT (..), runIdentityT)
import Control.Monad.Trans.Maybe (MaybeT (..))
import Control.Monad.Trans.Reader (Reader, ReaderT (..), ask)
import Data.List (intercalate)
import qualified Data.Map.Lazy as M
import Data.Maybe (fromMaybe)
-- IdentityT
-- The simplest of transformers!
-- newtype IdentityT f a = IdentityT { runIdentityT :: f a }
identityMaybeInt :: IdentityT Maybe Int
identityMaybeInt = IdentityT (Just 10)
-- identityExample :: IdentityT Maybe Int
-- identityExample =
-- let result =
-- fmap (runIdentityT . (\a -> IdentityT (Just (a + 10)))) identityMaybeInt
-- in IdentityT $ join $ runIdentityT result
-- identityExample =
-- let add10 = fmap (runIdentityT . (\a -> IdentityT (Just (a + 10))))
-- in IdentityT $ join $ runIdentityT $ add10 identityMaybeInt
-- But this is quite cumbersome...
-- Let's think about the type signature of >>=
-- (>>=) :: m a -> (a -> m b) -> m b
--
-- Let's do some replacing:
-- m a => (IdentityT Maybe) Int
-- a -> m b => Int -> ((IdentityT Maybe) Int)
-- m b => (IdentityT Maybe) Int
-- So in our case m is `IdentityT Maybe` and we can use >>= as normal
-- identityExample' :: IdentityT Maybe Int
-- identityExample' = identityMaybeInt >>= (\a -> IdentityT $ Just (a + 10))
identityExample' :: IdentityT Maybe Int
identityExample' = identityMaybeInt >>= (\a -> pure (a + 10))
-- Note the beauty of the monad interface and `pure`, we could be applying
-- this function to both `IdentityT Maybe Int` and `Maybe Int` with no changes
myPrompt :: String -> IO String
myPrompt prompt = do
putStr prompt
getLine
nicePrint :: Maybe String -> IO ()
nicePrint result =
putStrLn $
intercalate
"\n"
[ "==============================",
fromMaybe "An error occurred..." result,
"=============================="
]
naiveTry :: IO ()
naiveTry = do
name <- myPrompt "Name? "
if name /= ""
then do
phoneNumber <- myPrompt "Phone Number? "
if length phoneNumber < 10
then do
streetName <- myPrompt "Street Name? "
if streetName /= ""
then
nicePrint
( Just $
"Name: "
++ name
++ "\nPhone Number: "
++ phoneNumber
++ "\nStreet Name: "
++ streetName
)
else pure ()
else pure ()
else pure ()
fixedTry :: IO ()
fixedTry = do
name <- myPrompt "Name? "
guard $ name /= ""
phoneNumber <- myPrompt "Phone Number? "
guard $ length phoneNumber < 10
streetName <- myPrompt "Street Name? "
guard $ streetName /= ""
nicePrint $
Just $
"Name: "
++ name
++ "\nPhone Number: "
++ phoneNumber
++ "\nStreet Name: "
++ streetName
-- This works but it's not very flexible.
-- Let's look at MaybeT
-- newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
getNameBroken :: MaybeT IO String
getNameBroken = do
-- BROKEN: input <- myPrompt "Name? "
input <- lift $ myPrompt "Name? "
if input == ""
then MaybeT $ return Nothing
else MaybeT $ return $ Just $ "Name: " ++ input ++ "\n"
-- Unfortunately, this doesn't compile because when we're using monadic do
-- notation each line has to evaluate to the same monad. Instead `myPrompt`
-- returns a `IO String` instead of a `MaybeT IO String`
-- So we need this function:
-- `IO String -> MaybeT IO String`
-- Or a more generic one for all monads
-- `m a -> MaybeT m a`
-- Or a more generic one for all transformers
-- `m a -> t m a`
-- Surprise, this function exists and is called lift!
getName :: MaybeT IO String
getName = do
input <- lift $ myPrompt "Name? "
if input == ""
then MaybeT $ return Nothing
else MaybeT $ return $ Just $ "Name: " ++ input ++ "\n"
getNumber :: String -> MaybeT IO String
getNumber str = do
input <- lift $ myPrompt "Phone number? "
if input == ""
then MaybeT $ return Nothing
else MaybeT $ return $ Just $ str ++ "Phone Number: " ++ input ++ "\n"
getStreetName :: String -> MaybeT IO String
getStreetName str = do
input <- lift $ myPrompt "Street Name? "
if input == ""
then MaybeT $ return Nothing
else MaybeT $ return $ Just $ str ++ "Street Name: " ++ input
allTogether :: MaybeT IO String
allTogether = getName >>= getNumber >>= getStreetName
runAllTogetherNow :: IO ()
runAllTogetherNow = do
result <- runMaybeT allTogether
nicePrint result
-- The only thing which is weird is that getName, getNumber, and getStreetName
-- have different type signatures. We can rewrite them so they look exactly the same.
-- Also, we don't need to explicitly specify the return types.
getNameM :: MaybeT IO String
getNameM = do
input <- lift $ myPrompt "Name? "
if input == "" || length input > 10
then empty
else return $ "Name: " ++ input
getNumberM :: MaybeT IO String
getNumberM = do
input <- lift $ myPrompt "Phone number? "
if input == "" || length input > 10
then empty
else return $ "Phone Number: " ++ input
getStreetNameM :: MaybeT IO String
getStreetNameM = do
input <- lift $ myPrompt "Street Name? "
if input == "" || length input > 10
then empty
else return $ "Street Name: " ++ input
allTogetherM :: MaybeT IO String
allTogetherM = intercalate "\n" <$> sequence [getNameM, getNumberM, getStreetNameM]
runAllTogetherNowM :: IO ()
runAllTogetherNowM = do
result <- runMaybeT allTogetherM
nicePrint result
-- Reader and ReaderT:
-- type Reader r = ReaderT r Identity
-- newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
readerIntIOString :: ReaderT Int IO String
readerIntIOString =
ReaderT $ \int ->
if int > 10
then pure "VERY BIG"
else pure "smal"
readerExample :: IO ()
readerExample = do
result <- runReaderT readerIntIOString 11
putStrLn result
-- Let's try with a more real-world example
type Env = M.Map String Int
testEnv :: Env
testEnv =
M.fromList [("maxLength", 3)]
exampleGetter :: ReaderT Env IO Int
exampleGetter =
-- Naive version
-- ReaderT $ \env ->
-- pure $ fromMaybe 0 . (M.lookup "maxLength" env)
fromMaybe 0 . M.lookup "maxLength" <$> ask
envReaderExample :: IO ()
envReaderExample = do
result <- runReaderT exampleGetter testEnv
print result
-- Let's write a more generic version now
getEnv :: String -> ReaderT Env IO Int
getEnv key = fromMaybe 0 . M.lookup key <$> ask
getNameWithEnv :: MaybeT (ReaderT Env IO) String
getNameWithEnv = do
input <- lift $ lift $ myPrompt "Name? "
maxLength <- lift $ getEnv "maxLength"
if input == "" || length input > maxLength
then empty
else return $ "Name: " ++ input
getNumberWithEnv :: MaybeT (ReaderT Env IO) String
getNumberWithEnv = do
input <- lift $ lift $ myPrompt "Phone number? "
maxLength <- lift $ getEnv "maxLength"
if input == "" || length input > maxLength
then empty
else return $ "Phone Number: " ++ input
getStreetNameWithEnv :: MaybeT (ReaderT Env IO) String
getStreetNameWithEnv = do
input <- lift $ lift $ myPrompt "Street Name? "
maxLength <- lift $ getEnv "maxLength"
if input == "" || length input > maxLength
then empty
else return $ "Street Name: " ++ input
allTogetherWithEnv :: MaybeT (ReaderT Env IO) String
allTogetherWithEnv =
intercalate "\n"
<$> sequence [getNameWithEnv, getNumberWithEnv, getStreetNameWithEnv]
runWithEnv :: IO ()
runWithEnv = do
putStr "Max Length? "
maxLength <- readLn
result <-
runReaderT
(runMaybeT allTogetherWithEnv)
(M.fromList [("maxLength", maxLength :: Int)])
nicePrint result
-- Now we have customizable validations using Monad Transformers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment