Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Монады

Добро пожаловать в часть 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 isInt значение и объединить с maybeFunc2, чей тип Int -> Maybe Float.Float. TheОператор bindbind(>>=) operatorразрешает resolvesзначение theseв to a Maybe Float.Float. ThenЗатем 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.Int. TheОператор 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функцию.

to
continually

Мы accumulateникогда argumentsне inиспользуем anonymous functions. One note about do notation: we never use <- toдля unwrapразвернывания theпоследней finalоперации operationв inблоке ado.

do-block.
Our

Наш 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.