Монады
Добро пожаловать в часть 3 нашей серии абстрактных структур! Мы, наконец, коснемся идеи монад! Множежство людей пытаются изучить монады без попытки заиметь понимания того. как абстрактные структуры типов класса работают. Это главная причина борьбы. Если вы всё еще этого не пониматете, обратитесь к 1 и 2 части этой серии.
После этой статьи вы будете готовы к тому, чтобы писать свой собственный код Haskell.
Букварь монад
Есть множество инструкций и поисаний монад в интернете. Количество аналогий просто смешно. Но вот мои 5 копеек в определении: Монада - обертка значения или вычисления с определенным контекстом. Монада должна определять и смысл обернутого значения в контексте и способ объединения вычислений в контексте.
Это определение достатоно широко. Давайте взглянем на конкретный пример, и попробуем понять.
Классы типы монад
Так же как с функторами и аппликативными функторами, Haskell отражает монады с помощью тип класса. На это есть две функции:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
Эти две функции отвечают двум идеям выше. Функция возвращения определяем как обернуть значения в контексте монад. Оператор >>=
, который мы назовем его функцией "связывания", определяет как объединить две операции с контекстом. Давайте проясним это далее узучив несколько определенным экземпляров монад.
Монада Maybe
Just
asкак Maybe
isэто aфунктор functorи andаппликативный anфунктор, applicativeно functor,еще itи isмонада. alsoЧтобы aпонять monad.смысл Toмонады motivateMaybe
theдавайте Maybeпосмотрим monad,представим let's consider this code.код:
maybeFunc1 :: String -> Maybe Int
maybeFunc1 "" = Nothing
maybeFunc1 str = Just $ length str
maybeFunc2 :: Int -> Maybe Float
maybeFunc2 i = if i `mod` 2 == 0
then Nothing
else Just ((fromIntegral i) * 3.14159)
maybeFunc3 :: Float -> Maybe [Int]
maybeFunc3 f = if f > 15.0
then Nothing
else Just [floor f, ceiling f]
runMaybeFuncs :: String -> Maybe [Int]
runMaybeFuncs input = case maybeFunc1 input of
Nothing -> Nothing
Just i -> case maybeFunc2 i of
Nothing -> Nothing
Just f -> maybeFunc3 f
WeМожно canувидеть, seeчто we'reмы startingначинаем toразрабатывать developотвратительный aтреугольный hideousшаблон, triangleв patternкачестве asпродолжения weшаблона continueсоответствия patternрезультатов matchingуспешного onвызова theфункций. resultsЕсли ofмы successiveдобавили functionеще calls.больше Ifфункций weMaybe
wereв toнего, addто moreвсё станет еще хуже. Если мы считаем Maybe
functionsв ontoкачестве this,монады, itмы wouldможем keepсделать gettingкод worse.гораздо Whenчище. weДавайте considerвзглянем Maybeна asто, a monad, we can make the code much cleaner. Let's take a look at howкак Haskell implementsреализует Maybe
asмонаду, aчтобы monadпонять toкак seeэто how.делать.
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
Just a >>= f = f a
TheВнутри contextMaybe
theмонада Maybe monad describes is simple. Computations in Maybe can either fail or succeed with a value. We can take any value and wrap it in this context by calling the value a "success"проста. WeВычисления doсозначением thisв withMaybe
theмогут как пройти, так и не пройти успешно. Мы можем взять любое значение обернуть его в этом контексте вызовом значения success
. Мы делаем это с помощью конструктора Just
. constructor.Неуспех Weобозначается representс failureпомощью by Nothing.Nothing
.
WeОбъединим combineвычисления computationsв inконтексте thisпроверяя contextрезульта byпервого examiningвычисления. theЕсли resultуспешно, ofмы theберем firstего computation.значение Ifи itпередаем succeeded,во weвторое takesвычисление. itsЕсли value,неуспешно, andтогда passу itнас toнет theзначения secondдля computation.передачи Ifдальше. itПоэтому failed,результирующее thenвычисление weне haveбудет noуспешно. valueВзглянем toна passто, toкак theмы nextможем step.исопльзовать Sobind(>>=)
theоператор totalдля computationобъединения isнаших a failure. Let's look at how we can use the bind operator to combine our operations:операторов:
runMaybeFuncsBind :: String -> Maybe [Int]
runMaybeFuncsBind input = maybeFunc1 input >>= maybeFunc2 >>= maybeFunc3
ThisВыглядит looksгораздо muchчище! cleaner!Давайте Let'sвзглянем seeпочему whyработают theтипы. typesРезультат workmaybeFunc1
out.просто The result of maybeFunc1 input is simply Maybe Int. Then the bind operator allows us to take this Maybe Int
. valueЗатем andоператор combinebind(>>=)
itпозволяет withнам maybeFunc2,взять whoseэто typeMaybe
значение и объединить с isIntmaybeFunc2
, чей тип Int -> Maybe
. Float.FloatTheОператор bindbind(>>=)
operatorразрешает resolvesзначение theseв to a Maybe
. Float.FloatThenЗатем weмы passпередаем thisпоходим similarlyобразом throughчерез theоператор bindbind(>>=)
operatorв tomaybeFunc3
maybeFunc3,результатом resultingкоторой inявляется ourконечный finalтип: type, Maybe [Int]
.
YourВаша functionsфункции willне notвсегд alwaysбудут combineтак soясно cleanlyсочитаться. though.Тут Thisв isсилу whereвступает запись do
. notationКод comesвыше intoможно play.переписать Weследующим can rewrite the above as:образом:
runMaybeFuncsDo :: String -> Maybe [Int]
runMaybeFuncsDo input = do
i <- maybeFunc1 input
f <- maybeFunc2 i
maybeFunc3 f
TheОператор <-
operatorособенный. isОн special.эффективно Itразворачивает effectivelyзначение unwrapsс theправой valueстороны onмонады. theЭто right-handзначит, sideчто fromзначение thei
monad.имеет Thisтипа meansInt
, theдаже valueне iсмотря hasна typeрезультат Int,maybeFunc1
evenкак though the result of maybeFunc1 is Maybe
. Int.IntTheОператор bindbind(>>=)
operationработает happensбез underнашего theучастия. hood.Если Ifфункция theвозвращает functionNothing
, returnsтогда Nothing,вся thenфункция therunMaybeFuncs
entireвернет runMaybeFuncs function will return Nothing.Nothing
.
AtПри firstбеглом glance,осмотре, thisэто looksвыглядит moreгораздо complicatedсложнее, thanчем theпример bindс example.bind(>>=)
. However,Однако, itоно givesдает usнам aгораздо lotбольше moreгибкости. flexibility.Предположим, Considerмы ifхотим we wanted to addдобавить 2 toк theцелому integerчислу beforeперед callingвызовом maybeFunc2.MaybeFunc2
. ThisЭто isпроще easyсделать toс dealпомощью withdo
inзаписи, doно notation,гораздо butсложноее moreиспользуя difficult when simply using binds:связывания.
runMaybeFuncsDo2 :: String -> Maybe [Int]
runMaybeFuncsDo2 input = do
i <- maybeFunc1 input
f <- maybeFunc2 (i + 2)
maybeFunc3 f
-- Not so nice
runMaybeFuncsBind2 :: String -> Maybe [Int]
runMaybeFuncsBind2 input = maybeFunc1 input
>>= (\i -> maybeFunc2 (i + 2))
>>= maybeFunc3
TheПреимущества gainsгораздо areочевидны evenесли moreмы obviousхотим ifиспользовать weмножество wantпрошедших toрезультатов useпри multipleвызове previousфункии. resultsИспользуя inсвязывания, aмы functionсможем call.постоянно Usingскладывать binds,аргументы weв wouldанонимную haveфункцию.
continuallyМы
accumulateникогдаargumentsнеinиспользуемanonymous functions. One note about do notation: we never use<-
toдляunwrapразверныванияtheпоследнейfinalоперацииoperationвinблокеado
.do-block.
Наш callвызов tomaybeFunc3
maybeFunc3имеет hasтип the type Maybe [Int]
. ThisЭто isнаш ourпоследний final type тип(notне [Int]) soпоэтому weего doне notнужно unwrap it.разворачивать.
THEмонада EITHER MONADEither
Now,Теперь, let'sдавайте examineпосмотрим theна монаду Either
, monad,которая whichочень isпохожа quiteна similarмонаду toMaybe
. theВот Maybeеё monad. Here's the definition:определение:
instance Monad (Either a) where
return r = Right r
(Left l) >>= _ = Left l
(Right r) >>= f = f r
WhereasПоскольку theMaybe
имеет успех или не успех со значением, монада Either
прикладывает информацию к неуспеху. Just
как Maybe
eitherобертывает succeedsзначение withв aего valueконтексте orвызова fails,делая theего Eitherупешным. monadМонадическое attachesповедение informationтак toже failures.объединяет Justоперации likeзавершаясь Maybe,на itпервом wrapsне valuesуспехе. inДавайте itsпосмотрим contextкак byмы callingможем themиспользовать successful.это Theчтобы monadicсделать behaviorнаш alsoкод combinesвыше operations by short-circuiting on the first failure. Let's see how we can use this to make our code from above more clear.чище.
eitherFunc1 :: String -> Either String Int
eitherFunc1 "" = Left "String cannot be empty!"
eitherFunc1 str = Right $ length str
eitherFunc2 :: Int -> Either String Float
eitherFunc2 i = if i `mod` 2 == 0
then Left "Length cannot be even!"
else Right ((fromIntegral i) * 3.14159)
eitherFunc3 :: Float -> Either String [Int]
eitherFunc3 f = if f > 15.0
then Left "Float is too large!"
else Right [floor f, ceiling f]
runEitherFuncs :: String -> Either String [Int]
runEitherFuncs input = do
i <- eitherFunc1 input
f <- eitherFunc2 i
eitherFunc3 f
Before,Любой everyне failureуспех justпросто gaveдаст usнам aзначение Nothing
: value:
>> runMaybeFuncs ""
Nothing
>> runMaybeFuncs "Hi"
Nothing
>> runMaybeFuncs "Hithere"
Nothing
>> runMaybeFuncs "Hit"
Just [9,10]
Nowкогда whenмы weзапустим runнаш ourкод, code,мы weможем canпосмотреть lookна atстроковый theрезультат resultingошибки, errorи string,она andрасскажет thisнам willо tellтом, usкакая whichфункция functionне actuallyсмогла failed.произвести вычисления.
>> runMaybeFuncs ""
Left "String cannot be empty!"
>> runMaybeFuncs "Hi"
Left "Length cannot be even!"
>> runMaybeFuncs "Hithere"
Left "Float is too large!"
>> runMaybeFuncs "Hit"
Right [9,10]
NoticeЗаметим, weчто parameterizeмы theпараметризовали монаду Either
monadс byпомощью theнашего errorтипа type.ошибки. IfЕсли weу have:нас есть:
data CustomError = CustomError
maybeFunc2 :: Either CustomError Float
...
ThisЭто functionфункция isтеперь inновая aмонада. differentОбъединения monadс now.другими Itфункциями won'tне beбудет quite as simple to combine this with our other functions. If you're curious how we might do this, check out this answer on quora.легким.
THEМонада IO MONAD
TheМонада IOIO, Monadвозмоно, isсамая perhapsважная theмонада most important monad inв Haskell. ItЭто isтак alsoже oneодна ofиз theсамых hardestсложных monadsмонад toдля understandпонимания startingначинающих. out.Её Itsреализация actualдостаточна implementationсложна isдля aобсуждения bitпри tooпервом intricateзнакомстве toс discussязыком. whenПоэтому firstбудем learningучиться monads.по So we'll learn by example.примерам.
The IO monad wraps computations in the following context: "This computation can read information from or write information to the terminal, file system, operating system, and/or network". If you want to get user input, print a message to the user, read information from a file, or make a network call, you'll need to do so within the IO Monad. These are "side effects". We cannot perform them from "pure" Haskell code.
The most important job of pretty much any computer program is to interact with the outside world in some way. For this reason, the root of all executable Haskell code is a function called main, with the type IO (). So every program starts in the IO monad. From here you can get any input you need, call into relatively "pure" code with the inputs, and then output the result in some way. The reverse does not work. You cannot call into IO code from pure code like you can call into a Maybe function from pure code.
Let's look at a simple program showing a few of the basic IO functions. We'll use do-notation to illustrate the similarity to the other monads we've discussed. We list the types of each IO function for clarity.
main :: IO ()
main = do
-- getLine :: IO String
input <- getLine
let uppercased = map Data.Char.toUpper input
-- print :: String -> IO ()
print uppercased
So we see once again each line of our program has type IO a. (A let statement can occur in any monad). Just as we could unwrap i in the maybe example to get an Int instead of a Maybe Int, we can use <- to unwrap the result of getLine as a String. We can then manipulate this value using string functions, and pass the result to the print function.
This is a simple echo program. It reads a line from the terminal, and then prints the line back out in all caps. Hopefully it gives you a basic understanding of how IO works. We'll get into more details in the next couple articles.
SUMMARY
At this point, we should finally have a decent grasp on what monads are. But if they don't make sense yet, don't fret! It took me a few different tries before I really understood them! Don't be afraid to take another look at part 1 and part 2 to give yourself a refresher on Haskell structure basics. And definitely feel free to read this article again!
But if you are feeling good, then you're ready to move on to part 4, where you'll learn about the Reader and Writer monads. These start to bring us access to some of the functionality you think Haskell might be "missing".
If you've never programmed in Haskell before, hopefully I've convinced you that it's not that scary and you're ready to check it out! Download our Beginners Checklist to learn how to get started.