Что такое монада? - PullRequest
       289

Что такое монада?

1330 голосов
/ 05 сентября 2008

Кратко рассмотрев недавно Хаскелла, каким было бы краткое, краткое, практичное объяснение того, что в сущности является монадой?

Я обнаружил, что большинство объяснений, с которыми я столкнулся, было довольно недоступным и лишено практических деталей.

Ответы [ 45 ]

1015 голосов
/ 11 октября 2008

Первый: термин монада немного пустоват, если вы не математик. Альтернативный термин - построитель вычислений , который немного больше описывает то, для чего они на самом деле полезны.

Вы запрашиваете практические примеры:

Пример 1. Понимание списка :

[x*2 | x<-[1..10], odd x]

Это выражение возвращает двойные числа всех нечетных чисел в диапазоне от 1 до 10. Очень полезно!

Оказывается, это действительно просто синтаксический сахар для некоторых операций в монаде List. Такое же понимание списка можно записать так:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

Или даже:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

Пример 2: ввод / вывод :

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

В обоих примерах используются монады, построители вычислений AKA. Общая тема состоит в том, что монада связывает операции определенным, полезным способом. В понимании списка операции объединены в цепочку, так что если операция возвращает список, то следующие операции выполняются для каждого элемента в списке. Монада ввода-вывода, с другой стороны, выполняет операции последовательно, но передает «скрытую переменную», которая представляет «состояние мира», что позволяет нам писать код ввода-вывода чисто функциональным способом.

Оказывается, шаблон операций с цепочками довольно полезен и используется для множества разных вещей в Haskell.

Другой пример - исключения: при использовании монады Error операции объединяются в цепочку так, что они выполняются последовательно, за исключением случаев, когда выдается ошибка, и в этом случае оставшаяся часть цепочки отбрасывается.

И синтаксис со списком, и с нотацией являются синтаксическим сахаром для операций цепочки с использованием оператора >>=. Монада - это просто тип, который поддерживает оператор >>=.

Пример 3: анализатор

Это очень простой парсер, который анализирует либо строку в кавычках, либо число:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

Операции char, digit и т. Д. Довольно просты. Они либо совпадают, либо не совпадают. Волшебство - это монада, которая управляет потоком управления: операции выполняются последовательно до тех пор, пока не произойдет сбой совпадения, и в этом случае монада возвращается к последнему <|> и пробует следующую опцию. Опять же, способ объединения операций с некоторой дополнительной полезной семантикой.

Пример 4. Асинхронное программирование

Приведенные выше примеры есть в Haskell, но оказывается, F # также поддерживает монады. Этот пример украден у Don Syme :

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

Этот метод выбирает веб-страницу. Изюминкой является использование GetResponseAsync - оно фактически ожидает ответа в отдельном потоке, в то время как основной поток возвращается из функции. Последние три строки выполняются в порожденном потоке, когда ответ получен.

В большинстве других языков вам придется явно создать отдельную функцию для строк, которые обрабатывают ответ. Монада async способна «разделить» блок самостоятельно и отложить выполнение второй половины. (Синтаксис async {} указывает, что поток управления в блоке определяется монадой async.)

Как они работают

Так, как монада может делать все эти причудливые вещи управления потоком? Что действительно происходит в do-блоке (или вычислительном выражении , как их называют в F #), так это то, что каждая операция (в основном каждая строка) заключена в отдельную анонимную функцию. Затем эти функции объединяются с помощью оператора bind (пишется >>= в Haskell). Поскольку операция bind объединяет функции, она может выполнять их так, как считает нужным: последовательно, несколько раз, в обратном порядке, отбрасывать некоторые, выполнять некоторые в отдельном потоке, когда чувствует себя так, и так далее.

В качестве примера, это расширенная версия IO-кода из примера 2:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

Это уродливее, но также более очевидно, что на самом деле происходит. Оператор >>= является магическим ингредиентом: он принимает значение (слева) и объединяет его с функцией (справа), чтобы получить новое значение. Это новое значение затем берется следующим оператором >>= и снова объединяется с функцией для получения нового значения. >>= можно рассматривать как мини-оценщик.

Обратите внимание, что >>= перегружен для разных типов, поэтому каждая монада имеет собственную реализацию >>=. (Все операции в цепочке должны быть одного типа, но оператор >>= не будет работать.)

Самая простая из возможных реализаций >>= просто берет значение слева и применяет его к функции справа и возвращает результат, но, как уже было сказано, полезность всего шаблона заключается в том, что происходит что-то дополнительное. в реализации монады >>=.

Существует некоторая дополнительная хитрость в том, как значения передаются от одной операции к следующей, но это требует более глубокого объяснения системы типов Haskell.

Подведение итогов

В терминах Haskell монада - это параметризованный тип, который является экземпляром класса типов Monad, который определяет >>= вместе с несколькими другими операторами. С точки зрения непрофессионала, монада - это просто тип, для которого определена операция >>=.

Сам по себе >>= - это всего лишь громоздкий способ объединения функций, но при наличии нотации, скрывающей «слесарное дело», монадические операции оказываются очень хорошей и полезной абстракцией, полезной во многих местах. на языке, и полезно для создания собственных мини-языков на языке.

Почему монады тяжелы?

Для многих изучающих Хаскель монады являются препятствием, которое они наносят, как кирпичная стена. Дело не в том, что сами монады являются сложными, а в том, что реализация опирается на многие другие расширенные функции Haskell, такие как параметризованные типы, классы типов и так далее. Проблема заключается в том, что ввод / вывод Haskell основан на монадах, и ввод / вывод, вероятно, является одной из первых вещей, которую вы хотите понять при изучении нового языка - в конце концов, создавать программы, которые не производят никаких программ, не очень интересно. выход. У меня нет немедленного решения этой проблемы «курица-яйцо», за исключением обработки ввода-вывода как «волшебство происходит здесь», пока у вас не будет достаточно опыта работы с другими частями языка. К сожалению.

Отличный блог по монадам: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

684 голосов
/ 20 апреля 2012

Объяснение "что такое монада" немного похоже на выражение "что такое число?" Мы используем номера все время. Но представьте, что вы встретили человека, который ничего не знал о числах. Как, черт возьми, вы бы объяснили, что это за числа? И как бы вы начали описывать, почему это может быть полезно?

Что такое монада? Краткий ответ: это особый способ объединения операций.

По сути, вы пишете шаги выполнения и связываете их вместе с помощью "функции связывания". (В Haskell он называется >>=.) Вы можете записать вызовы в оператор связывания самостоятельно или использовать синтаксический сахар, который заставляет компилятор вставлять эти вызовы функций для вас. Но в любом случае каждый шаг отделяется вызовом этой функции связывания.

Таким образом, функция связывания похожа на точку с запятой; он разделяет шаги в процессе. Задача функции связывания состоит в том, чтобы взять выходные данные с предыдущего шага и передать их на следующий шаг.

Звучит не так уж сложно, верно? Но существует более чем один вид монады. Зачем? Как?

Ну, функция связывания может просто взять результат с одного шага и передать его на следующий шаг. Но если это "все", что делает монада ... это на самом деле не очень полезно. И это важно понимать: каждая полезная монада делает что-то еще в дополнение к тому, чтобы быть просто монадой. Каждая полезная монада обладает «особой силой», которая делает ее уникальной.

(Монада, которая ничего не делает , называется «монадой идентичности». Скорее, как функция идентичности, это звучит как совершенно бессмысленная вещь, но оказывается, что это не так ... Но это другое история и торговли;.)

По сути, каждая монада имеет собственную реализацию функции связывания. И вы можете написать функцию связывания так, чтобы она выполняла определенные действия между этапами выполнения. Например:

  • Если каждый шаг возвращает индикатор успеха / неудачи, bind может выполнить следующий шаг, только если предыдущий был успешным. Таким образом, неудачный шаг прерывает всю последовательность «автоматически», без каких-либо условных проверок с вашей стороны. ( Отказ Монада .)

  • Расширяя эту идею, вы можете реализовать «исключения». ( Ошибка Monad или Exception Monad .) Поскольку вы определяете их сами, а не как языковую функцию, вы можете определить, как они работают. (Например, может быть, вы хотите игнорировать первые два исключения и прерывать работу только тогда, когда выдается третье исключение.)

  • Вы можете заставить каждый шаг возвращать многократные результаты , и иметь над ними петлю функции связывания, вводя каждый из них в следующий шаг для вас. Таким образом, вам не нужно постоянно писать циклы повсеместно при работе с несколькими результатами. Функция связывания "автоматически" сделает все это за вас. ( Список Монад .)

  • Помимо передачи «результата» от одного шага к другому, вы можете использовать функцию связывания для передачи дополнительных данных . Эти данные теперь не отображаются в вашем исходном коде, но вы все равно можете получить к ним доступ из любого места, без необходимости вручную передавать их каждой функции. ( Читатель Monad .)

  • Вы можете сделать так, чтобы «дополнительные данные» можно было заменить. Это позволяет вам имитировать деструктивные обновления , фактически не делая деструктивных обновлений. ( Государственная Монада и ее кузина Писательская Монада .)

  • Поскольку вы только имитируете деструктивные обновления, вы можете тривиально делать вещи, которые были бы невозможны с помощью real деструктивных обновлений. Например, вы можете отменить последнее обновление или вернуться к более старой версии .

  • Вы можете создать монаду, в которой вычисления могут быть приостановлены , так что вы можете приостановить вашу программу, войти и поработать с внутренними данными состояния, а затем возобновить ее.

  • Вы можете реализовать «продолжения» как монаду. Это позволяет вам сломать умы людей!

Все это и многое другое возможно с монадами. Конечно, все это также вполне возможно без монад тоже. Это просто радикально проще с использованием монад.

177 голосов
/ 16 сентября 2008

На самом деле, вопреки общему пониманию монад, они не имеют ничего общего с государством. Монады - это просто способ обернуть вещи и предоставить методы для выполнения операций над обернутыми вещами, не разворачивая их.

Например, вы можете создать тип для переноса другого в Haskell:

data Wrapped a = Wrap a

Чтобы обернуть вещи мы определяем

return :: a -> Wrapped a
return x = Wrap x

Для выполнения операций без развертывания, скажем, у вас есть функция f :: a -> b, тогда вы можете сделать это для lift , которая действует на упакованные значения:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

Это все, что нужно понять. Однако оказывается, что есть более общая функция для этого подъема , то есть bind:

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bind может сделать немного больше, чем fmap, но не наоборот. На самом деле, fmap может быть определено только в терминах bind и return. Итак, при определении монады вы указываете ее тип (здесь это был Wrapped a), а затем говорите, как работают ее операции return и bind.

Круто то, что это такой общий паттерн, который всплывает повсюду, инкапсулируя состояние в чистом виде - только один из них.

Для хорошей статьи о том, как монады можно использовать для введения функциональных зависимостей и, таким образом, управления порядком оценки, как это используется в монаде IO Haskell, ознакомьтесь с IO Inside .

Что касается понимания монад, не беспокойтесь об этом. Читайте о них то, что вам интересно, и не беспокойтесь, если не поймете сразу. Тогда просто погрузиться на таком языке, как Хаскелл. Монады - это одна из тех вещей, где понимание проникает в ваш мозг практикой, однажды вы вдруг понимаете, что понимаете их.

167 голосов
/ 05 августа 2008

Но, Вы могли бы изобрести Монады!

sigfpe говорит:

Но все они представляют монады как нечто эзотерическое, нуждающееся в объяснении. Но я хочу сказать, что они вовсе не эзотерические. Фактически, столкнувшись с различными проблемами в функциональном программировании, вы неизбежно привели бы к определенным решениям, которые все являются примерами монад. На самом деле, я надеюсь заставить вас изобретать их сейчас, если вы этого еще не сделали. Это маленький шаг, чтобы заметить, что все эти решения на самом деле являются одним и тем же замаскированным решением. И после прочтения этого вам, возможно, будет легче понять другие документы о монадах, потому что вы узнаете все, что видите, как то, что вы уже изобрели.

Многие проблемы, которые пытаются решить монады, связаны с проблемой побочных эффектов. Итак, начнем с них. (Обратите внимание, что монады позволяют вам делать больше, чем просто справляться с побочными эффектами, в частности, многие типы объектов-контейнеров можно рассматривать как монады. В некоторых введениях в монады трудно совместить эти два различных использования монад и сосредоточиться только на другой.)

В императивном языке программирования, таком как C ++, функции ведут себя не так, как функции математики. Например, предположим, что у нас есть функция C ++, которая принимает один аргумент с плавающей запятой и возвращает результат с плавающей запятой. Внешне это может показаться чем-то похожим на математическую функцию, отображающую реальное в реальное, но функция C ++ может сделать больше, чем просто вернуть число, зависящее от ее аргументов. Он может читать и записывать значения глобальных переменных, а также записывать вывод на экран и получать ввод от пользователя. Однако на чистом функциональном языке функция может читать только то, что ей предоставлено в своих аргументах, и единственный способ, которым она может влиять на мир, - это возвращаемые значения.

83 голосов
/ 05 сентября 2008

Монада - это тип данных, который имеет две операции: >>= (он же bind) и return (он же unit). return принимает произвольное значение и создает экземпляр монады вместе с ним. >>= берет экземпляр монады и отображает над ним функцию. (Вы уже можете видеть, что монада - это странный тип данных, поскольку в большинстве языков программирования вы не можете написать функцию, которая принимает произвольное значение и создает из него тип. Монады используют вид параметрический полиморфизм .)

В нотации Haskell интерфейс монады написан

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

Предполагается, что эти операции подчиняются определенным «законам», но это не очень важно: «законы» просто кодифицируют способ, которым должны вести себя разумные реализации операций (в основном, что >>= и return должны согласовываться о том, как значения преобразуются в экземпляры монад и что >>= является ассоциативным).

Монады - это не только состояние и ввод / вывод: они абстрагируют общую схему вычислений, которая включает в себя работу с состоянием, вводом / выводом, исключениями и недетерминизмом. Вероятно, простейшие для понимания монады - это списки и типы опций:

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

, где [] и : - конструкторы списка, ++ - оператор конкатенации, а Just и Nothing - конструкторы Maybe. Обе эти монады инкапсулируют общие и полезные шаблоны вычислений для их соответствующих типов данных (обратите внимание, что ни один из них не имеет ничего общего с побочными эффектами или вводом / выводом).

Вы действительно должны поиграть в написание некоторого нетривиального кода на Haskell, чтобы оценить, что такое монады и почему они полезны.

74 голосов
/ 27 сентября 2008

Сначала вы должны понять, что такое функтор. Перед этим разберитесь с функциями высшего порядка.

A функция высшего порядка - это просто функция, которая принимает функцию в качестве аргумента.

A функтор - это конструкция любого типа T, для которой существует функция высшего порядка, назовите ее map, которая преобразует функцию типа a -> b (для любых двух типов a и b) в функцию T a -> T b. Эта map функция также должна подчиняться законам идентичности и композиции, так что следующие выражения возвращают true для всех p и q (нотация Haskell):

map id = id
map (p . q) = map p . map q

Например, конструктор типа с именем List является функтором, если он снабжен функцией типа (a -> b) -> List a -> List b, которая подчиняется приведенным выше законам. Единственная практическая реализация очевидна. Результирующая функция List a -> List b выполняет итерацию по заданному списку, вызывая функцию (a -> b) для каждого элемента и возвращает список результатов.

A монада - это просто функтор T с двумя дополнительными методами: join, типа T (T a) -> T a и unit (иногда называемый return , fork или pure) типа a -> T a. Для списков в Haskell:

join :: [[a]] -> [a]
pure :: a -> [a]

Почему это полезно? Потому что вы можете, например, map над списком с функцией, которая возвращает список. Join берет полученный список списков и объединяет их. List - это монада, потому что это возможно.

Вы можете написать функцию, которая выполняет map, затем join. Эта функция называется bind, или flatMap, или (>>=), или (=<<). Это обычно, как экземпляр монады дается в Haskell.

Монада должна удовлетворять определенным законам, а именно, что join должна быть ассоциативной. Это означает, что если у вас есть значение x типа [[[a]]], тогда join (join x) должно равняться join (map join x). И pure должен быть идентификатором для join таким, что join (pure x) == x.

44 голосов
/ 17 сентября 2008

[Отказ от ответственности: я все еще пытаюсь полностью впасть в монады. Вот что я понял до сих пор. Если это не так, надеюсь, кто-то знающий позвонит мне на ковер.]

Арнар написал:

Монады - это просто способ обернуть вещи и предоставить методы для выполнения операций над обернутым материалом, не разворачивая его.

Именно так. Идея звучит так:

  1. Вы берете какое-то значение и добавляете в него дополнительную информацию. Точно так же, как значение определенного типа (например, целое число или строка), так и дополнительная информация имеет определенный вид.

    Например, эта дополнительная информация может быть Maybe или IO.

  2. Тогда у вас есть несколько операторов, которые позволяют вам работать с обернутыми данными, сохраняя при этом эту дополнительную информацию. Эти операторы используют дополнительную информацию, чтобы решить, как изменить поведение операции для переносимого значения.

    Например, Maybe Int может быть Just Int или Nothing. Теперь, если вы добавите Maybe Int к Maybe Int, оператор проверит, находятся ли они оба Just Int s внутри, и, если это так, развернет Int s, передайте им оператор сложения, повторно -оберните полученный Int в новый Just Int (который является действительным Maybe Int) и, таким образом, верните Maybe Int. Но если один из них был Nothing внутри, этот оператор просто немедленно вернет Nothing, что снова является действительным Maybe Int. Таким образом, вы можете притвориться, что ваши Maybe Int являются просто нормальными числами и выполнять с ними регулярные вычисления. Если бы вы получили Nothing, ваши уравнения все равно дадут правильный результат - без необходимости мусорить проверки для Nothing везде .

Но пример - это то, что происходит с Maybe. Если дополнительная информация была IO, то вместо этого вызывался бы этот специальный оператор, определенный для IO, и он мог сделать что-то совершенно другое перед выполнением сложения. (Хорошо, добавление двух IO Int вместе, вероятно, бессмысленно - я еще не уверен.) (Кроме того, если вы обратили внимание на пример Maybe, вы заметили, что «упаковка значения с дополнительными вещами» не является всегда правильно. Но трудно быть точным, правильным и точным, не будучи непостижимым.)

По сути, «монада» примерно означает «рисунок» . Но вместо книги, полной неформально объясненных и специально названных шаблонов, теперь у вас есть языковая конструкция - синтаксис и все - что позволяет вам объявлять новые шаблоны как элементы вашей программы . (Неточность здесь заключается в том, что все шаблоны должны следовать определенной форме, поэтому монада не так универсальна, как модель. Но я думаю, что это самый близкий термин, который большинство людей знают и понимают.)

И именно поэтому люди находят монады настолько запутанными: потому что они - такая общая концепция. Спрашивать, что делает что-то монадой, так же неопределенно, как спрашивать, что делает что-то образцом.

Но подумайте о последствиях наличия синтаксической поддержки в языке для идеи шаблона: вместо того, чтобы читать книгу Gang of Four и запоминать конструкцию определенного шаблона, вы просто напишите код, который реализует этот шаблон в едином родовом виде один раз, и тогда все готово! Затем вы можете повторно использовать этот шаблон, например Visitor, Strategy, Façade или любой другой, просто украсив им операции в вашем коде, без необходимости повторной реализации его снова и снова!

Так вот почему люди, которые понимают монады, находят их такими полезными : интеллектуальные снобы гордятся пониманием не какой-то концепции башни из слоновой кости (хорошо, конечно, тоже ), но на самом деле делает код проще.

41 голосов
/ 06 ноября 2010

После долгих попыток, я думаю, я наконец понял монаду. Перечитав мою собственную длинную критику ответа с подавляющим большинством голосов, я предложу это объяснение.

Для понимания монад необходимо ответить на три вопроса:

  1. Зачем тебе монада?
  2. Что такое монада?
  3. Как реализована монада?

Как я отмечал в своих первоначальных комментариях, слишком много монадических объяснений попадают в вопрос № 3, прежде чем он действительно адекватно охватывает вопрос 2 или вопрос 1.

Зачем вам нужна монада?

Чистые функциональные языки, такие как Haskell, отличаются от императивных языков, таких как C или Java, тем, что чисто функциональные программы не обязательно выполняются в определенном порядке, по одному шагу за раз. Программа на Haskell больше похожа на математическую функцию, в которой вы можете решить «уравнение» в любом количестве возможных порядков. Это дает ряд преимуществ, среди которых заключается в том, что исключает возможность возникновения ошибок определенного типа, особенно тех, которые связаны с такими вещами, как «состояние».

Однако есть некоторые проблемы, которые не так просто решить с помощью этого стиля программирования. Некоторые вещи, такие как консольное программирование и файловый ввод-вывод, должны происходить в определенном порядке или поддерживать состояние. Один из способов решения этой проблемы - создать объект, который представляет состояние вычислений, и ряд функций, которые принимают объект состояния в качестве входных данных и возвращают новый измененный объект состояния.

Итак, давайте создадим гипотетическое значение «state», которое представляет состояние экрана консоли. как именно это значение создается, не важно, но допустим, что это массив символов ascii длиной в байтах, который представляет то, что в данный момент видно на экране, и массив, который представляет последнюю строку ввода, введенную пользователем, в псевдокоде. Мы определили некоторые функции, которые принимают состояние консоли, изменяют его и возвращают новое состояние консоли.

consolestate MyConsole = new consolestate;

Таким образом, для консольного программирования, но чисто функциональным образом, вам нужно будет вкладывать много вызовов функций друг в друга.

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

Программирование таким способом сохраняет «чистый» функциональный стиль, заставляя изменения в консоли происходить в определенном порядке. Но мы, вероятно, захотим сделать больше, чем просто несколько операций за один раз, как в приведенном выше примере. Вложенные функции таким образом начнут становиться неловкими. То, что мы хотим, - это код, который по сути делает то же самое, что и выше, но написан немного больше так:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

Это действительно был бы более удобный способ написать это. Как мы это делаем, хотя?

Что такое монада?

Если у вас есть тип (например, consolestate), который вы определяете вместе с набором функций, специально предназначенных для работы с этим типом, вы можете превратить весь пакет этих вещей в «монаду», определив оператор например, : (связывание), который автоматически подает возвращаемые значения слева, в параметры функции справа, и оператор lift, который превращает нормальные функции, в функции, которые работают с оператором связывания определенного типа.

Как реализована монада?

Посмотрите другие ответы, которые кажутся совершенно свободными в деталях.

34 голосов
/ 07 сентября 2008

(См. Также ответы на Что такое монада? )

Хорошей мотивацией для монад является sigfpe (Дан Пипони) Вы могли бы изобрести монады! (А может быть, у вас уже есть) . Существует МНОГО других учебных пособий по монадам , многие из которых ошибочно пытаются объяснить монады «простыми терминами», используя различные аналогии: это ошибка учебного пособия по монаде ; избегайте их.

Как говорит DR MacIver в Скажите нам, почему ваш язык отстой :

Итак, что я ненавижу в Хаскеле:

Давайте начнем с очевидного. Монады учебники. Нет, не монады. В частности, учебники. Они бесконечные, раздутые и дорогой бог, они утомительны. Кроме того, я никогда не видел убедительных доказательств того, что они действительно помогают. Прочитайте определение класса, напишите некоторый код, переберите страшное имя.

Вы говорите, что понимаете монаду "Может быть"? Хорошо, ты уже в пути. Просто начните использовать другие монады, и рано или поздно вы поймете, что такое монады в целом.

[Если вы математически ориентированы, вы можете игнорировать десятки учебных пособий и выучить определение или следовать лекциям по теории категорий :) Основная часть определения состоит в том, что Monad M включает в себя «конструктор типов», который определяет для каждого существующего типа «T» новый тип «MT», и некоторые способы перехода назад и вперед между «обычными» типами и «M». типы.]

Также, как ни странно, одно из лучших введений в монады - это одна из ранних академических работ, представляющих монады, монады Филиппа Уодлера для функционального программирования . На самом деле в нем есть практические, нетривиальные мотивирующие примеры, в отличие от многих искусственных учебных пособий.

32 голосов
/ 08 августа 2014

Я написал это в основном для себя, но я надеюсь, что другие найдут это полезным:)

Я полагаю, что это объяснение является более правильным. Однако я думаю, что это лечение все еще ценно и рассмотрит его включение позже. Достаточно сказать, что там, где обычная композиция функций имеет дело с простыми значениями функций, монады предназначены для составления функций, которые оперируют значениями функций (функциями более высокого порядка). Когда вы имеете дело с функциями более высокого порядка (функциями, которые принимают или возвращают функции), композиция должна быть настроена или адаптирована так, чтобы оценивать операнды при оценке композиции. Этот процесс оценки может быть экзотическим, например, сбор результатов асинхронных процессов. Тем не менее, этот пошив может быть выполнен по образцу. Версия этого паттерна называется Monad и очень сильно следует алгебраическим сложениям. В частности, в отношении следующего содержания такие функции более высокого порядка будут рассматриваться как математические операторы в выражении, принимающие в качестве операндов другие частично примененные операторы, и, таким образом, функции 1+ 2 *, 3 / и 7+ в 1+ ( 2* ( 3/ ( 7+ (..) ) ) ) ...

Монады решают проблему, которая также отображается в арифметике как деление на ноль, DivByZero. В частности, вычисления, связанные с делением, должны обнаруживать или допускать исключение DivByZero. Это требование затрудняет кодирование таких выражений в общем случае.

Монадическое решение состоит в том, чтобы охватить DivByZero, выполнив следующее

  1. Разверните тип Number, включив в него DivByZero в качестве определенного значения, которое не является обычным числом: NaN, Infinity или Null. Давайте назовем этот новый тип номера, Nullable<Number>.
  2. Предоставляет функцию для «поднятия» или упаковки существующего Number в тип Nullable<Number> (идея «оборачивания» заключается в том, что содержимое Number или значение может быть «развернуто» без потери информации)
  3. Предоставляет функцию для "подъема" или переноса существующих операторов на Number в версии, которые работают на Nullable<Number>. Такой результирующий «поднятый» оператор может просто сделать следующее:
    1. разверните предоставленные Nullable<Number> операнды и примените к ним содержащийся в них оператор Number, затем "поднимите" полученный Number результат в Nullable<Number>
    2. обнаруживает DivByZero операнд или исключение во время оценки и путем дальнейшей оценки, производя значение DivByZero в качестве результата для подтверждения этого (1 + Null = Null). Однако, какие действия предпринять, зависит от программиста. В общем, эти функции-обертки - это то, где написано много функциональных возможностей монад. Информация о монадическом состоянии поддерживается в самом типе оболочки, откуда проверяются упакованные функции и, в соответствии с подходом неизменяемости функционального программирования, создают новое монадическое значение. В случае Nullable<Number> такая монадическая информация о состоянии описывает, существует ли DivByZero или фактический Number.

Итак, Monad - это расширенный тип вместе с функцией, которая «оборачивает» исходный тип в эту расширенную версию, и другой функцией, которая оборачивает исходные операторы, чтобы они могли обрабатывать этот новый расширенный тип. (Монады могли быть мотивом для обобщений или параметров типа.)

Оказывается, что вместо простого сглаживания обработки DivByZero (или Infinity, если хотите), обработка Monad широко применима к ситуациям, которые могут выиграть от расширения типов для упрощения их кодирования. На самом деле эта применимость кажется широкой.

Например, IO Monad - это тип, который буквально представляет вселенную. Намерение состоит в том, чтобы признать, что значения, возвращаемые прототипной программой HelloWorld, не полностью описываются типом результата string и его значением «Hello World!». Фактически, такой результат также включает в себя модификации аппаратных средств и состояний памяти таких устройств, как консоль. Например, после выполнения консоль теперь отображает дополнительный текст, курсор находится на новой строке и т. Д. IO Monad - это просто явное признание таких внешних или побочных эффектов, если хотите.

Зачем беспокоиться?

Монады позволяют разрабатывать алгоритмы без сохранения состояния и документировать алгоритмы с полным состоянием. Полноценные машины сложны. Например, машина с 10 битами может находиться в 2 ^ 10 возможных состояниях. Устранение лишней сложности - идеал функциональных языков.

Переменные удерживают состояние. Устранение «переменных» должно просто напичкать. Чисто функциональные программы не обрабатывают переменные, а только значения (несмотря на использование термина 'переменная' в документации на Haskell) и вместо этого используют метки, символы или имена для таких значений по мере необходимости. Следовательно, самая близкая вещь к переменной в чисто функциональном языке - это параметры, полученные функцией, так как они принимают новые значения при каждом вызове. (Метка относится к значению, тогда как переменная относится к месту, где хранится значение. Следовательно, вы можете изменить содержимое переменной, но метка - это само содержимое. В конечном итоге лучше дать яблоко, чем мешок с яблоком, возможно, в нем.)

Отсутствие переменных объясняет, почему чисто функциональные языки используют рекурсию вместо циклов для итерации. Инкремент счетчика включает в себя использование переменной, которая становится инкрементной, и всю неопределенность с тем, как она обновляется, когда она тестируется, какое значение она должна иметь и когда, а затем сложность, когда у вас есть несколько потоков, потенциально обращающихся к этому та же переменная.

Тем не менее, и что?

Без присутствия состояния функция должна стать декларацией или определением ее результатов, в отличие от зачисления некоторого базового состояния в сторону результата. По сути, функциональное выражение incFun(x) = x + 1 проще, чем императивное выражение incImp(x) = x.add(1); return x; Здесь incFun не изменяет x, но создает новое значение. incFun может даже быть заменен его определением в выражениях: 1 + incFun(x) становится 1 + (x + 1). С другой стороны, incImp изменяет состояние x. Что бы ни означало такое изменение для x, может быть неясно и, в конечном счете, его невозможно определить без выполнения программы в дополнение к каким-либо проблемам параллелизма.

Такая сложность становится когнитивно дорогой со временем (2 ^ N). Напротив, оператор, +, не может изменить x, но вместо этого должен создать новое значение, результат которого ограничен и полностью определяется значениями x и 1 и определением + . В частности, предотвращается взрыв сложности 2 ^ N. Кроме того, чтобы подчеркнуть параллелизм, incImp, в отличие от incFun, не может быть вызван одновременно без мер предосторожности при совместном использовании параметра, поскольку он становится модифицированным при каждом вызове.

Зачем называть это Монадой?

Монада характеризуется математической структурой, называемой моноидом из теории алгебраических групп. С учетом сказанного все это означает, что моноид имеет следующие три свойства:

  1. имеет бинарный оператор, *, такой, что x * y = z для x, y, and z, принадлежащего некоторому типу S. Например, 1 & делить; 2 = 0,5, где 1, 2 и 0,5 все имеют тип Number. Закрыто
  2. имеет элемент идентификации, i, связанный с бинарным оператором, который не делает ничего такого, что (i * x) = (x * i) = x. Например, числовой оператор + и число 0 в 4 + 0 = 0 + 4 = 4. Идентичность
  3. порядок оценки «сегментов» не имеет значения: (x * y) * z = x * (y * z). Например, числовой оператор + в (3 + 4) + 12 = 3 + (4 + 12) = 19. Обратите внимание, однако, что последовательность терминов не должна меняться. ассоциативность

Свойство три (Ассоциативность) позволяет оценивать выражения произвольной длины, разделяя их на сегменты и оценивая каждый сегмент независимо, например, параллельно. Например, x1*x2*...*xN может быть сегментирован на (x1..xJ) * (xJ+1...xM) * (xM+1...xN). Отдельный результат, x0J * xJM * xMN, может быть затем собран и дополнительно оценен аналогично. Подобная поддержка сегментации является ключевой техникой, обеспечивающей правильный параллелизм и распределенную оценку в соответствии с алгоритмами распределенного поиска Google (а-ля карта / уменьшение).

Свойство два (Идентичность) позволяет упростить построение выражений различными способами, хотя это может быть не совсем очевидно; однако, точно так же, как ноль, очевидно, не был необходим для ранних систем подсчета, он полезен как концепция пустого, так и для переноса пустого значения. Обратите внимание, что в типе Nullable<Number>, Null не пустое значение, а DivByZero. В частности, nn + DivByZero = DivByZero, тогда как nn + 0 = 0 + nn = nn, следовательно, 0 остается идентификатором в +, где nn является любым Nullable<Number>.

Наконец, есть причина, по которой мы больше не используем римские цифры ... нет расширенного приспособления для нуля или дробей, иррациональных чисел, отрицательных чисел, мнимых чисел, ... да, похоже, наша система счисления может рассматриваться монада

...