Преобразователи Монад
В нескольких прошлых частях серии, мы изучили множество новых монад. В 3 части мы увидели как часто вещи как Maybe и IO могут быть монадами. Затем в 4 и 5 частях мы изучили Reader, Writer и State монады. С этими монадами на поясе, вы возмоно думаете как можно их объединять. Ответ, как мы обнаружи в этой части, это преобразователи монад.
С пониманием монад, вы открываете больше Haskell возможностей. Но вам всё ещё нужны идеи библиотек Haskell, который позволят вам их испытать.
Пример Мотивации
Ранее, мы уже видели как монада maybe помогает избежать треугольника судьбы шаблонов кода. Без них, нам нужно проверять каждую функцию на успех. Однако, примеры на которые мы смотрим, где всё является чистым кодом предполагает следующее:
main1 :: IO ()
main1 = do
maybeUserName <- readUserName
case maybeUserName of
Nothing -> print "Invalid user name!"
Just (uName) -> do
maybeEmail <- readEmail
case maybeEmail of
Nothing -> print "Invalid email!"
Just (email) -> do
maybePassword <- readPassword
Case maybePassword of
Nothing -> print "Invalid Password"
Just password -> login uName email password
readUserName :: IO (Maybe String)
readUserName = do
putStrLn "Please enter your username!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readEmail :: IO (Maybe String)
readEmail = do
putStrLn "Please enter your email!"
str <- getLine
if '@' `elem` str && '.' `elem` str
then return $ Just str
else return Nothing
readPassword :: IO (Maybe String)
readPassword = do
putStrLn "Please enter your Password!"
str <- getLine
if length str < 8 || null (filter isUpper str) || null (filter isLower str)
then return Nothing
else return $ Just str
login :: String -> String -> String -> IO ()
...
В этом примере, все наши потенциальные проблемы кода идут из IO монады. Как мы може использовать Maybe монаду когда мы уже внутри другой монады?
Преобразователи Монад
К счастью, мы можем получить желаемое поведение используя преобразователи монад для объединения. В этом примере, мы обернем IO действиее внутрь преобразованной монады MaybeT.
Преобразователи Монад это оберточный тип. В общем параметризируемый другим монадическим типом. Затем вы можете запустить действие из внутренней монады, в то время пока добавляете ваше собственное поведение для действия объединения в новую монаду. Общий преобразователь добавляет T в конец существующей монады. Ниже представленно определение MaybeT:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
instance (Monad m) => Monad (MaybeT m) where
return = lift . return
x >>= f = MaybeT $ do
v <- runMaybeT x
case v of
Nothing -> return Nothing
Just y -> runMaybeT (f y)
MaybeT сам по себе это newtype. Он содержит обертку над значением Maybe. Если тип m это monad, мы можем так же сделать монаду из MaybeT.
Представим наш пример. Мы хотим использовать MaybeT для оборачивания IO монады, чтобы запустить IO действия. Это значит, что наша новая монада MaybeT IO. Наши три вспомогательные функции все возвращают строки, поэтому каждая из них получает тип MaybeT IO String. Для преобразования старого IO кода в MaybeT монаду, всё, что нужно - обернуть IO действие в MaybeT конструктор.
readUserName' :: MaybeT IO String
readUserName' = MaybeT $ do
putStrLn "Please enter your Username!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readEmail' :: MaybeT IO String
readEmail' = MaybeT $ do
putStrLn "Please enter your Email!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readPassword' :: MaybeT IO String
readPassword' = MaybeT $ do
putStrLn "Please enter your Password!"
str <- getLine
if length str < 8 || null (filter isUpper str) || null (filter isLower str)
then return Nothing
else return $ Just str
Теперь ы можем обернуть все три этих вызова в одно монадическое действие, и сделать простое сравнение для получения результата. Мы воспользуемся runMaybeT функцией для развертывания значения Maybe из MaybeT:
main2 :: IO ()
main2 = do
maybeCreds <- runMaybeT $ do
usr <- readUserName
email <- readEmail
pass <- readPassword
return (usr, email, pass)
case maybeCreds of
Nothing -> print "Couldn't login!"
Just (u, e, p) -> login u e p
И этот новый код бдует иметь правильное простое поведение для Maybe монады. Если какая-то функция read упадет, наш код сразу же вернет Nothing.
Добавление уровней.
Вот и мы дождались долгожданное части о преобразователях монад. Так как наш новосозданный тип сам по себе монада, мы межем обернуть её внутри другого преборазователя. Почти все распространненые монады имеют преобразователь типа, MaybeT в том числе, это преобразователь для обычной Maybe монады.
Для быстрого примера, предположим, у нас есть Env тип содержащий пользовательскую информацию. Мы можем обернуть это окружение в Reader. Однако, мы хоти всё еще иметь доступ к IO функциональности, поэтму мы воспользумся Reader преобразователем. Затем обернем резултат с помощью MaybeT.
type Env = (Maybe String, Maybe String, Maybe String)
readUserName'' :: MaybeT (ReaderT Env IO) String
readUserName'' = MaybeT $ do
(maybeOldUser, _, _) <- ask
case maybeOldUser of
Just str -> return $ Just str
Nothing -> do
-- lift allows normal IO functions from inside ReaderT Env IO!
lift $ putStrLn "Please enter your Username!"
input <- lift getLine
if length input > 5
then return (Just input)
else return Nothing
NoticeЗаметим, weчто hadу toнам useнужно использовать lift toдля runзапуска theIO IOфункции functiongetLine. getLine.В Inпреобразователе aмонады, monadlift transformer,функция theпозволяет liftнам functionзапустить allowsдействия youнижележащей toмонады. runЭто actionsповедение inзахватывается theклассом underlying monad. This behavior is encompassed by the MonadTrans: class:
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
SoИспользование usinglift liftв in the ReaderT Env IO actionдействии allowsпозвляет IO functions.функцияю UsingИспользование theтипа typeшаблона templateиз fromкласса, theмы class,можем weзаменить can substitute Reader Env forна t,t andи IO forна m.m.
WithinВнутри a MaybeT (ReaderT Env IO) function,функции, callingвызываемой lift wouldпозволяет allowвам youзапустить toфункцию runReader. aНам не нужно то что выше, так как набор кода лежит в Reader function.действии Weв don'tобертке needMaybeT this above since the bulk of the code lies in Reader actions wrapped by the MaybeT constructor.конструктора.
ToЧтобы understandпонять theидею conceptлифтинга, ofподумайте lifting,о thinkуровне ofвашей yourмонады monadкак layerо asстеке. aКогда stack.вы Whenимеете you have a ReaderT Env IO action,действие, imagineпредставьте, aчто Reader Env monadмонада onсверху topIO ofмонады. theIO IOдействие monad.лежит Anна IOнижнем actionуровне. existsПоэтому, onчтобы theзапустить bottomвсё layer.это Soдело toс runверхнего itслоя, fromвам theнужно upperсначала layer,подняться. youЕсли needваш toстек liftимеет itбольше up.чем If2 yourслоя, stackвы isможете moreподниматься thanнесколько twoраз. layers,Вызывая youдважды can lift multiple times. Calling lift twice from the MaybeT (ReaderT Env IO) monadмонаду willпозволит allowвам youвызывать toIO call IO functions.функцию.
It'sНе inconvenientудобно toкаждый haveраз toзнать knowсколько howраз manyтебе timesнужно toвызывать callфункцию lift toдля getполучения toтекущего aуровня. particularОтсюда levelвспомогательная ofфункйия theчасто chain.используется Thusдля helperэтого. functionsВдобавок, areпосле frequentlyпреобразования usedмонады, forможно this.запустить Additionally,несколько sinceуровней, monadтипы transformersмогут canстановится runсложнее. severalПоэтому layersобычно deep,используют theбиблиотеку types can get complicated. So it is typical to use type synonyms. liberally.
type TripleMonad a = MaybeT (ReaderT Env IO) a
performReader :: ReaderT Env IO a -> TripleMonad a
performReader = lift
performIO :: IO a -> TripleMonad a
performIO = lift . lift
TYPECLASSESТипоклассы
AsВ aкачестве similarпохожей idea,идеи, thereесть aretypeclass someкоторый typeclassesпозволяет whichнам allowсделать youопределенные toпредположения makeо certainстеке assumptionsмонады. aboutДля theпримера, monadвас stackчасто below.не Forволнует, instance,что youименно oftenв don'tстеке, careно whatвам theнужен exactIO stackгде-то is,внутри. butВ youэтом justи needзаключается IOцель toиспользования existMondaIO somewhere on the stack. This is the purpose of the MonadIO typeclass:типокласса.
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
We can use this behavior to get a function to print even when we don't know its exact monad:
debugFunc :: (MonadIO m) => String -> m ()
debugFunc input = liftIO $ putStrLn ("Successfully produced input: " ++ input)
SoДаже evenне thoughсмотря thisна functionто, doesn'tчто explicitlyфункция liveявно inне находится в MaybeT , IO,IOweмы canможем writeнаписать aнашу versionверсию ofmain ourфункции mainчтобы functionиспользовать to use it.её.
main3 :: IO ()
main3 = do
maybeCreds <- runMaybeT $ do
usr <- readUserName'
debugFunc usr
email <- readEmail'
debugFunc email
pass <- readPassword'
debugFunc pass
return (usr, email, pass)
case maybeCreds of
Nothing -> print "Couldn't login!"
Just (u, e, p) -> login u e p
OneВыfinalнеnote:можете,Youвcannot,общем,inобернутьgeneral,другуюwrapмонадуanotherсmonadпомощьюwithIOtheмонады используя преобразователь. Однако, можно сделать другое монадическое значение чтобы вренуть типIOmonadдействия.using a transformer. You can, however, make the other monadic value the return type of an IO action.
func :: IO (Maybe String)
-- This type makes sense
func2 :: IO_T (ReaderT Env (Maybe)) string
-- This does not exist
SUMMARYВыводы
NowТеперь, thatвы youзнаете, knowкак howобъединять toваши combineмонады, yourвы monadsпочти together,завершили you'reпонинмание almostключевых doneидей! withВы, understandingвозможно, theхотите keyпопробовать conceptsначать ofписать monads!достаточно Youсложный couldкод. probablyНо, goчтобы outнаучиться nowвладеть andмонадами, startвам writingнужно someзнать prettyкак complexделать code!свою Butсобственную toмонаду, trulyи masterдля monads,этого youвам shouldнужно knowпонять howпоследню toидею. makeЭто yourидея own,типа and there's one final concept that you should understand for that. This is the idea of type "laws"laws. EachКаждая ofструктура, theкоторую structuresмы we'veпрошли goneв overэтой inчасти thisлекций, seriesсвязана hasс alaws. seriesИ ofчтобы ваши примеры имели смысл, они должны следовать laws(т.е. associatedзакону). with it. And for your instances of these classes to make sense, they should follow the laws! Check out partПроверьте 7 toглаву, makeчтобы sureпонять, youпонимаете knowли what'sвы goingчто on!происходит.
Now that you can write some pretty complex code, you need to know some of the libraries that will help you use it! Download our Production Checklist for a summary of some awesome libraries to help you apply your skills! Haskell has many tools for tasks like building web APIs and a