В функциональном программировании монада — это структура, которая объединяет фрагменты программы ( функции ) и обертывает их возвращаемые значения в тип с дополнительными вычислениями. Помимо определения монадического типа- обертки , монады определяют два оператора : один для переноса значения в монадический тип, а другой для объединения функций, выводящих значения монадического типа (они известны как монадические функции ). Языки общего назначения используют монады для сокращения шаблонного кода, необходимого для общих операций (таких как работа с неопределенными значениями или ошибочными функциями или инкапсуляция бухгалтерского кода). Функциональные языки используют монады для превращения сложных последовательностей функций в краткие конвейеры, абстрагирующие поток управления и побочные эффекты . [1] [2]
И понятие монады, и сам термин изначально пришли из теории категорий , где монада определяется как функтор с дополнительной структурой. [a] Исследования, начавшиеся в конце 1980-х и начале 1990-х годов, установили, что монады могут объединить, казалось бы, разрозненные проблемы информатики в единую функциональную модель. Теория категорий также предоставляет несколько формальных требований, известных как законы монады , которым должна удовлетворять любая монада и которые можно использовать для проверки монадического кода. [3] [4]
Поскольку монады делают семантику явной для определенного вида вычислений, их также можно использовать для реализации удобных функций языка. Некоторые языки, такие как Haskell , даже предлагают в своих основных библиотеках готовые определения для общей структуры монады и общих экземпляров. [1] [5]
«Для монады m
значение типа m a
представляет собой доступ к значению типа a
в контексте монады». —К.А. Макканн [6]
Точнее, монада может использоваться там, где неограниченный доступ к значению неуместен по причинам, специфичным для данного сценария. В случае с монадой Maybe это происходит потому, что значение может не существовать. В случае с монадой ввода-вывода это связано с тем, что значение может быть еще не известно, например, когда монада представляет пользовательский ввод, который будет предоставлен только после отображения приглашения. Во всех случаях сценарии, в которых доступ имеет смысл, фиксируются операцией привязки, определенной для монады; для монады Maybe значение связывается только в том случае, если оно существует, а для монады IO значение связывается только после выполнения предыдущих операций в последовательности.
Монаду можно создать, определив конструктор типа M и две операции:
return :: a -> M a
(часто также называемый unit ), который получает значение типа a
и оборачивает его в монадическое значение типа M a
, иbind :: (M a) -> (a -> M b) -> (M b)
(обычно представленный как >>=
), который получает функцию f
по типу a
и может преобразовывать монадические значения M a
, применяясь f
к развернутому значению a
, возвращая монадическое значение M b
.(Альтернативную, но эквивалентную конструкцию, использующую join
функцию вместо bind
оператора, можно найти в следующем разделе § Вывод из функторов .)
С помощью этих элементов программист составляет последовательность вызовов функций («конвейер») с несколькими операторами связывания , объединенными в выражение. Каждый вызов функции преобразует входное значение простого типа, а оператор связывания обрабатывает возвращенное монадическое значение, которое передается на следующий шаг последовательности.
Обычно оператор связывания >>=
может содержать уникальный для монады код, выполняющий дополнительные вычислительные шаги, недоступные в функции, полученной в качестве параметра. Между каждой парой вызовов составных функций оператор связывания может вводить в монадическое значение m a
некоторую дополнительную информацию, которая недоступна внутри функции f
, и передавать ее по конвейеру. Он также может обеспечить более точный контроль над потоком выполнения, например, вызывая функцию только при определенных условиях или выполняя вызовы функций в определенном порядке.
Одним из примеров монады является Maybe
тип. Неопределенные нулевые результаты — это особая болевая точка, для решения которой многие процедурные языки не предоставляют специальных инструментов, требующих использования шаблона нулевого объекта или проверок для проверки недопустимых значений в каждой операции для обработки неопределенных значений. Это вызывает ошибки и затрудняет создание надежного программного обеспечения, которое корректно обрабатывает ошибки. Тип Maybe
заставляет программиста иметь дело с этими потенциально неопределенными результатами, явно определяя два состояния результата: Just ⌑result⌑
, или Nothing
. Например, программист может создавать синтаксический анализатор, который должен возвращать промежуточный результат или сигнализировать об условии, которое синтаксический анализатор обнаружил и которое программист также должен обработать. Всего лишь немного дополнительных функциональных особенностей, и этот Maybe
тип превращается в полнофункциональную монаду. [б] : 12,3 страницы 148–151.
В большинстве языков монада Maybe также известна как тип параметра , который представляет собой тип, который отмечает, содержит ли он значение или нет. Обычно они выражаются как некий перечислимый тип . В этом примере Rust мы будем называть его Maybe<T>
, и варианты этого типа могут быть либо значением универсального типа T
, либо пустым вариантом: Nothing
.
// <T> представляет общий тип "T" enum Maybe < T > { Just ( T ), Nothing , }
Maybe<T>
также можно понимать как тип «обертки», и именно здесь проявляется его связь с монадами. В языках с той или иной формой этого типа Maybe
есть функции, которые помогают в их использовании, например составление монадических функций друг с другом и проверка, если a Maybe
содержит значение.
В следующем жестко запрограммированном примере Maybe
тип используется в результате функций, которые могут завершиться неудачно. В этом случае тип ничего не возвращает, если есть деление на ноль .
fn разделить ( x : Decimal , y : Decimal ) -> Maybe < Decimal > { if y == 0 { return Nothing } else { return Just ( x / y ) } } // разделить (1.0, 4.0) -> возвращает Just (0.25) // разделить(3.0, 0.0) -> ничего не возвращает
Одним из таких способов проверить, Maybe
содержит ли a значение, является использование if
операторов.
пусть m_x = делить ( 3,14 , 0,0 ); // см. функцию деления выше // Оператор if извлекает x из m_x, если m_x является вариантом Just Maybe if let Just ( x ) = m_x { println! ( "ответ: " , x ) } else { println! ( "деление не удалось, ошибка деления на ноль..." ) }
Другие языки могут иметь сопоставление с образцом.
пусть результат = разделить ( 3,0 , 2,0 ); результат совпадения { Just ( x ) => println! ( "Ответ: " , x ), Ничего => println! ( «разделение провалилось; мы достанем их в следующий раз». ), }
Монады могут составлять функции, возвращающие Maybe
, и объединять их. В конкретном примере одна функция может принимать несколько Maybe
параметров и возвращать один, Maybe
значение которого равно, Nothing
когда любой из параметров равен Nothing
, как показано ниже:
fn Chainable_division ( Maybe_x : Maybe < Decimal > , Maybe_y : Maybe < Decimal > ) -> Maybe < Decimal > { match ( Maybe_x , Maybe_y ) { ( Just ( x ), Just ( y )) => { // Если оба входа являются справедливыми, проверьте деление на ноль и разделите соответственно if y == 0 { return Nothing } else { return Just ( x / y ) } }, _ => return Nothing // В противном случае не возвращайте Nothing } } Chainable_division ( chainable_division ( Just ( 2.0 ), Просто ( 0,0 )), Просто ( 1,0 )); // внутри Chainable_division происходит сбой, снаружи Chainable_division не возвращает ничего
Чтобы переписать функции для использования Maybes в этом конкретном примере, требуется много шаблонного кода (посмотрите на все эти Just
выражения!). Вместо этого мы можем использовать так называемый оператор связывания . (также известный как «карта», «плоская карта» или «толчок» [8] : 2205s ). Эта операция принимает монаду и функцию, которая возвращает монаду, и запускает функцию для внутреннего значения переданной монады, возвращая монаду из функции.
// Пример Rust с использованием ".map". Maybe_x передается через две функции, которые возвращают Maybe<Decimal> и Maybe<String> соответственно. // Как и в случае с обычной композицией функций, входные и выходные данные функций, переходящих друг в друга, должны соответствовать обернутым типам. (т. е. функция add_one должна возвращать Maybe<Decimal>, который затем можно развернуть в Decimal для функции decimal_to_string) let Maybe_x : Maybe < Decimal > = Just ( 1.0 ) let Maybe_result = Maybe_x . карта ( add_one ). карта ( decimal_to_string )
В Haskell есть оператор связывания или ( >>=
), который позволяет использовать эту монадическую композицию в более элегантной форме, похожей на композицию функций . [с] : 150–151
пополам :: Int -> Maybe Int пополам x | даже x = Just ( x ` div ` 2 ) | нечетный x = ничего . Этот код вдвое уменьшает x пополам. его значение равно нулю, если x не кратно 4, пополам x >> = пополам
При >>=
наличии chainable_division
это можно выразить гораздо более лаконично с помощью анонимных функций (т.е. лямбда-выражений). Обратите внимание в приведенном ниже выражении, как каждая из двух вложенных лямбда-выражений работает с обернутым значением в переданной Maybe
монаде с помощью оператора привязки. [д] : 93
Chainable_division ( mx , my ) = mx >>= ( λx -> my >>= ( λy -> Just ( x / y )) )
То, что было показано до сих пор, по сути является монадой, но, если быть более кратким, ниже приводится строгий список качеств, необходимых для монады, как это определено в следующем разделе.
Maybe
) [б] : 148–151 Just(x)
) [d] : 93 >>=
или .flatMap()
) [c] : 150–151 Это три вещи, необходимые для образования монады. Другие монады могут воплощать разные логические процессы, а некоторые могут иметь дополнительные свойства, но все они будут иметь эти три схожих компонента. [1] [9]
Более распространенное определение монады в функциональном программировании, использованное в приведенном выше примере, на самом деле основано на тройке Клейсли ⟨T, η, µ⟩, а не на стандартном определении теории категорий. Однако эти две конструкции оказываются математически эквивалентными, поэтому любое определение даст допустимую монаду. Учитывая любые четко определенные базовые типы T , U , монада состоит из трех частей:
unit : T → M T
[ф]>>=
(>>=) : (M T, T → M U) → M U
[g] так что если mx : M T
и f : T → M U
, то (mx >>= f) : M U
Однако, чтобы полностью квалифицироваться как монада, эти три части также должны соблюдать несколько законов:
unit(x) >>= f
↔ f(x)
ma >>= unit
↔ ma
ma >>= λx → (f(x) >>= g)
↔ (ma >>= f) >>= g
[1]С алгебраической точки зрения это означает , что любая монада одновременно порождает категорию (называемую категорией Клейсли ) и моноид в категории функторов (от значений до вычислений), с монадической композицией в качестве бинарного оператора в моноиде [8] : 2450 и единица как тождество в монаде.
Ценность шаблона монады выходит за рамки простого сжатия кода и обеспечения связи с математическими рассуждениями. Какой бы язык или парадигму программирования по умолчанию ни использовал разработчик, следование шаблону монады приносит множество преимуществ чисто функционального программирования . Реифицируя определенный вид вычислений, монада не только инкапсулирует утомительные детали этого вычислительного шаблона, но и делает это декларативным образом , улучшая ясность кода. Поскольку монадические значения явно представляют не только вычисленные значения, но и вычисленные эффекты , монадическое выражение может быть заменено его значением в ссылочно прозрачных позициях , как и чистые выражения, что позволяет использовать множество методов и оптимизаций, основанных на переписывании . [4]
Обычно программисты используют привязку для объединения монадических функций в последовательность, что привело к тому, что некоторые стали описывать монады как «программируемые точки с запятой», что указывает на то, сколько императивных языков используют точки с запятой для разделения операторов . [1] [5] Однако следует подчеркнуть, что монады на самом деле не упорядочивают вычисления; даже в языках, в которых они используются в качестве центральных функций, более простая композиция функций позволяет упорядочить шаги внутри программы. Общая польза монады скорее заключается в упрощении структуры программы и улучшении разделения задач посредством абстракции. [4] [11]
Структуру монады также можно рассматривать как уникальную математическую и временную вариацию шаблона декоратора . Некоторые монады могут передавать дополнительные данные, недоступные функциям, а некоторые даже осуществляют более тонкий контроль над выполнением, например, вызывая функцию только при определенных условиях. Поскольку они позволяют программистам приложений реализовывать логику предметной области , одновременно выгружая шаблонный код в заранее разработанные модули, монады можно даже считать инструментом аспектно-ориентированного программирования . [12]
Еще одно примечательное применение монад — это изоляция побочных эффектов, таких как ввод/вывод или изменяемое состояние , в чисто функциональном коде. Даже чисто функциональные языки могут реализовать эти «нечистые» вычисления без монад, в частности, посредством сложной комбинации композиции функций и стиля передачи продолжения (CPS). [2] Однако в случае с монадами большую часть этих каркасов можно абстрагировать, по сути, взяв каждый повторяющийся шаблон в коде CPS и объединив его в отдельную монаду. [4]
Если язык по умолчанию не поддерживает монады, реализовать шаблон все равно возможно, зачастую без особых затруднений. При переводе из теории категорий в термины программирования структура монады является общей концепцией и может быть определена непосредственно на любом языке, который поддерживает эквивалентную функцию для ограниченного полиморфизма . Способность концепции оставаться независимой от операционных деталей при работе с базовыми типами является мощной, но уникальные особенности и строгое поведение монад отличают их от других концепций. [13]
Обсуждения конкретных монад обычно сосредотачиваются на решении узкой проблемы реализации, поскольку данная монада представляет собой определенную вычислительную форму. Однако в некоторых ситуациях приложение может даже достичь своих целей высокого уровня, используя соответствующие монады в своей основной логике.
Вот лишь несколько приложений, в основе которых лежат монады:
Термин «монада» в программировании на самом деле восходит к языкам программирования APL и J , которые имеют тенденцию быть чисто функциональными. Однако в этих языках «монада» — это всего лишь сокращение для функции, принимающей один параметр (функция с двумя параметрами является «диадой» и т. д.). [19]
Математик Роджер Годеман был первым, кто сформулировал концепцию монады (назвав ее «стандартной конструкцией») в конце 1950-х годов, хотя термин «монада», который стал доминировать, был популяризирован теоретиком категорий Сондерсом Мак Лейном . [ нужна цитация ] Форма, определенная выше с использованием связывания , однако, была первоначально описана в 1965 году математиком Генрихом Клейсли , чтобы доказать, что любая монада может быть охарактеризована как соединение между двумя (ковариантными) функторами. [20]
Начиная с 1980-х годов в компьютерном сообществе начало появляться смутное представление о монадном паттерне. По словам исследователя языка программирования Филипа Уодлера , ученый-компьютерщик Джон К. Рейнольдс предвидел несколько аспектов этого языка в 1970-х и начале 1980-х годов, когда он обсуждал ценность стиля передачи продолжения , теории категорий как богатого источника формальной семантики и различие типов между значениями и вычислениями. [4] Исследовательский язык Opal , который активно разрабатывался вплоть до 1990 года, также эффективно основывал ввод-вывод на монадическом типе, но в то время эта связь не была реализована. [21]
Ученый-компьютерщик Эудженио Моджи был первым, кто явно связал монаду теории категорий с функциональным программированием в докладе на конференции в 1989 году [22] , за которым последовала более подробная статья в журнале в 1991 году. В более ранних работах несколько ученых-компьютерщиков продвинулись вперед, используя теория категорий, обеспечивающая семантику лямбда -исчисления . Ключевая идея Моджи заключалась в том, что реальная программа — это не просто функция преобразования значений в другие значения, а скорее преобразование, которое формирует вычисления на основе этих значений. Если это формализовать в терминах теории категорий, это приводит к выводу, что монады представляют собой структуру, представляющую эти вычисления. [3]
Несколько других людей популяризировали и развивали эту идею, в том числе Филип Уодлер и Саймон Пейтон Джонс , оба участвовали в спецификации Haskell. В частности, Haskell использовал проблемную модель «ленивого потока» вплоть до версии 1.2 для согласования ввода-вывода с ленивым вычислением , пока не переключился на более гибкий монадический интерфейс. [23] Сообщество Haskell продолжало применять монады для решения многих задач функционального программирования, и в 2010-х годах исследователи, работающие с Haskell, в конечном итоге признали, что монады являются аппликативными функторами ; [24] [i] и что и монады, и стрелки являются моноидами . [26]
Поначалу программирование с использованием монад в основном ограничивалось Haskell и его производными, но поскольку функциональное программирование оказало влияние на другие парадигмы, многие языки включили монадный шаблон (если не по названию, то по духу). Формулировки теперь существуют в Scheme , Perl , Python , Racket , Clojure , Scala , F# , а также рассматриваются для нового стандарта машинного обучения . [ нужна цитата ]
Одним из преимуществ шаблона монады является обеспечение математической точности композиции вычислений. Для проверки достоверности экземпляра можно использовать не только законы монады, но и функции из связанных структур (например, функторы) посредством подтипирования .
Возвращаясь к Maybe
примеру, было объявлено, что его компоненты составляют монаду, но не было представлено никаких доказательств того, что они удовлетворяют законам монады.
Это можно исправить, вставив особенности Maybe
в одну сторону общих законов, а затем алгебраически построив цепочку равенств для достижения другой стороны:
Закон 1: эта(а) >>= f(x) ⇔ (Просто а) >>= f(x) ⇔ f(a)
Закон 2: ma >>= eta(x) ⇔ ma если ма (просто а) , то эта(а) ⇔ Просто иначе или Ничего ⇔ Ничего конец, если
Закон 3: ( ma >>= f(x) ) >>= g(y) ⇔ ma >>= ( f(x) >>= g(y) ) если (ma >>= f(x)) равно (Только b) , то если ma равно (Только а), то g(ma >>= f(x)) (f(x) >>= g(y)) a еще еще Ничего ничего конец, если конец, если ⇔ если ma равно (Просто а) и f(a) равно (Просто b), то (г ∘ е) а иначе, если ma равно (просто a), а f(a) равно ничем, то Ничего еще Ничего конец, если
Хотя в информатике это случается реже, но можно напрямую использовать теорию категорий, которая определяет монаду как функтор с двумя дополнительными естественными преобразованиями . [j] Итак, для начала структура требует функции высшего порядка (или «функционала») с именем map , чтобы квалифицироваться как функтор:
map φ : (a → b) → (ma → mb)
Однако это не всегда является серьезной проблемой, особенно когда монада получена из уже существующего функтора, после чего монада автоматически наследует карту . (По историческим причинам это map
называется fmap
в Haskell.)
Первое преобразование монады на самом деле представляет собой ту же самую единицу из тройки Клейсли, но, внимательно следя за иерархией структур, оказывается, что единица характеризует аппликативный функтор , промежуточную структуру между монадой и базовым функтором. В аппликативном контексте единицу иногда называют чистой , но это все та же функция. Что отличается в этой конструкции, так это то, что единица закона должна удовлетворять; поскольку привязка не определена, вместо этого ограничение задается в терминах карты :
(unit ∘ φ) x ↔ ((map φ) ∘ unit) x ↔ x
[27]Последний переход от аппликативного функтора к монаде происходит со вторым преобразованием, функцией соединения (в теории категорий это естественное преобразование, обычно называемое μ ), которое «сглаживает» вложенные приложения монады:
join(mma) : M (M T) → M T
В качестве характеристической функции соединение также должно удовлетворять трем вариантам законов монады:
(join ∘ (map join)) mmma ↔ (join ∘ join) mmma ↔ ma
(join ∘ (map unit)) ma ↔ (join ∘ unit) ma ↔ ma
(join ∘ (map map φ)) mma ↔ ((map φ) ∘ join) mma ↔ mb
Независимо от того, определяет ли разработчик прямую монаду или тройку Клейсли, базовая структура будет одинаковой, и формы можно легко выводить друг из друга:
(map φ) ma ↔ ma >>= (unit ∘ φ)
join(mma) ↔ mma >>= id
ma >>= f ↔ (join ∘ (map f)) ma
[28]Монада List естественным образом демонстрирует, как может оказаться полезным получение монады из более простого функтора. Во многих языках структура списка предопределена вместе с некоторыми базовыми функциями, поэтому List
конструктор типа и оператор добавления (представленные ++
инфиксной нотацией) считаются уже заданными здесь.
Встраивание простого значения в список также тривиально в большинстве языков:
единица измерения (х) = [х]
Отсюда итеративное применение функции с пониманием списка может показаться простым выбором для привязки и преобразования списков в полную монаду. Трудность этого подхода заключается в том, что метод связывания ожидает монадические функции, которые в этом случае сами будут выводить списки; по мере применения большего количества функций слои вложенных списков будут накапливаться, что потребует большего, чем базовое понимание.
Однако процедура применения любой простой функции ко всему списку, другими словами , карте , проста:
(отображение φ) xlist = [ φ(x1), φ(x2), ..., φ(xn) ]
Теперь эти две процедуры уже переходят List
в аппликативный функтор. Чтобы полностью квалифицироваться как монада, необходимо только правильное понятие объединения для выравнивания повторяющейся структуры, но для списков это просто означает развертывание внешнего списка для добавления внутренних, содержащих значения:
join(xlistlist) = join([xlist1, xlist2, ..., xlistn]) = xlist1 ++ xlist2 ++ ... ++ xlistn
Полученная монада представляет собой не только список, но и монада, размер которой автоматически изменяется и сжимается при применении функций. Теперь привязку можно также получить с помощью простой формулы, а затем использовать для передачи List
значений через конвейер монадических функций:
List
может значительно упростить использование многозначных функций, таких как комплексные корни. [29](xlist >>= f) = присоединиться ∘ (карта f) xlist
Одно из применений этого монадического списка — представление недетерминированных вычислений . List
может хранить результаты для всех путей выполнения в алгоритме, а затем сгущаться на каждом этапе, чтобы «забыть», какие пути к каким результатам привели (иногда важное отличие от детерминированных исчерпывающих алгоритмов). [ нужна цитата ]
Еще одним преимуществом является то, что проверки могут быть встроены в монаду; определенные пути можно прозрачно обрезать в первой точке сбоя, без необходимости переписывать функции в конвейере. [28]
Вторая ситуация, в которой List
есть смысл, — это составление многозначных функций . Например, комплексный корень n - го числа должен давать n различных комплексных чисел, но если затем из этих результатов берется еще один корень m -й степени, окончательные значения m•n должны быть идентичны выходным данным корня m•n- й степени. . полностью автоматизирует эту проблему, сжимая результаты каждого шага в плоский, математически правильный список. [29]List
Монады открывают возможности для интересных методов, выходящих за рамки простой организации логики программы. Монады могут заложить основу для полезных синтаксических функций, а их высокоуровневая математическая природа обеспечивает значительную абстракцию.
Хотя открытое использование связывания часто имеет смысл, многие программисты предпочитают синтаксис, который имитирует императивные операторы (так называемые do-notation в Haskell, Performance-notation в OCaml , вычислительные выражения в F# , [30] и для понимания в Scala ). Это всего лишь синтаксический сахар , который маскирует монадический конвейер под блок кода ; затем компилятор незаметно преобразует эти выражения в базовый функциональный код.
Перевод add
функции из Maybe
языка Haskell может продемонстрировать эту возможность в действии. Немонадическая версия add
Haskell выглядит так:
add mx my = case mx of Nothing -> Nothing Just x -> case my of Nothing -> Nothing Just y -> Just ( x + y )
В монадическом Haskell return
это стандартное имя для unit , плюс лямбда-выражения должны обрабатываться явно, но даже с учетом этих технических особенностей монада Maybe
обеспечивает более чистое определение:
добавить mx my = mx >>= ( \ x -> my >>= ( \ y -> return ( x + y )))
Однако с помощью do-нотации это можно еще больше свести к очень интуитивно понятной последовательности:
добавить mx my = do x <- mx y <- my return ( x + y )
Второй пример показывает, как Maybe
можно использовать совершенно другой язык: F#. С помощью вычислительных выражений функцию «безопасного деления», которая возвращает None
неопределенный операнд или деление на ноль, можно записать так:
let readNum () = let s = Console . ReadLine () let succ , v = Int32 . TryParse ( s ) if ( succ ) then Some ( v ) else Noneпусть secure_div = может быть { пусть ! x = readNum () пусть ! y = readNum () if ( y = 0 ) then None else return ( x / y ) }
Во время сборки компилятор внутренне «десахарит» эту функцию, превращая ее в более плотную цепочку вызовов связывания :
может быть . Delay ( fun () -> возможно . Bind ( readNum () , fun x -> возможно . Bind ( readNum () , fun y -> if ( y = 0 ) then None else возможно . Return ( x / y ))) )
Последний пример: даже сами общие законы монады могут быть выражены в до-нотации:
сделать { х <- вернуть v ; ж x } == do { f v } do { x <- m ; return x } == do { m } do { y <- do { x <- m ; х } ; г y } == do { x <- m ; y <- е х ; г й }
Несмотря на удобство, разработчик всегда должен помнить, что этот стиль блоков является чисто синтаксическим и может быть заменен внешне монадическими (или даже немонадическими CPS) выражениями. Использование связывания для выражения монадического конвейера во многих случаях все же может быть более понятным, а некоторые сторонники функционального программирования даже утверждают, что, поскольку блочный стиль позволяет новичкам переносить привычки императивного программирования, его следует избегать по умолчанию и использовать только в случае очевидного превосходства. [31] [1]
Каждой монаде нужна конкретная реализация, которая соответствует законам монады, но другие аспекты, такие как отношение к другим структурам или стандартным идиомам внутри языка, являются общими для всех монад. В результате язык или библиотека могут предоставлять общий Monad
интерфейс с прототипами функций , отношениями подтипов и другими общими фактами. Помимо обеспечения форы для разработки и гарантии того, что новая монада наследует функции супертипа (например, функторы), проверка дизайна монады на соответствие интерфейсу добавляет еще один уровень контроля качества. [ нужна цитата ]
Монадический код часто можно еще больше упростить за счет разумного использования операторов. Функционал карты может быть особенно полезен , поскольку он работает не только со специальными монадическими функциями; до тех пор, пока монадическая функция должна работать аналогично предопределенному оператору, карту можно использовать для мгновенного « преобразования » более простого оператора в монадический. [k]
С помощью этого метода определение add
из Maybe
примера можно свести к:
add(mx,my) = карта (+)
Этот процесс можно было бы продвинуть еще на один шаг вперед, определив add
не только for Maybe
, но и весь Monad
интерфейс. При этом любая новая монада, соответствующая интерфейсу структуры и реализующая собственную карту , сразу же унаследует расширенную версию add
. Единственное необходимое изменение в функции — это обобщение сигнатуры типа:
добавить: (Номер монады, Номер монады) → Номер монады [32]
Еще один монадический оператор, который также полезен для анализа, — это монадическая композиция ( >=>
здесь представленная в виде инфикса), которая позволяет объединять монадические функции в более математический стиль:
(f >=> g)(x) = f(x) >>= g
С помощью этого оператора законы монады можно записать только в терминах функций, подчеркивая соответствие ассоциативности и существованию тождества:
(единица >=> г) ↔ г(f >=> единица) ↔ f(f >=> g) >=> h ↔ f >=> (g >=> h) [1]
В свою очередь, приведенное выше показывает значение блока «do» в Haskell:
делать _p <- f(x) _q <- г(_p) h(_q) ↔ ( f >=> g >=> h )(x)
Самая простая монада — это монада Identity , которая просто аннотирует простые значения и функции, чтобы удовлетворить законам монады:
идентификатор нового типа T = Tединица(х) = х(х >>= е) = е(х)
Identity
на самом деле имеет допустимое применение, например, предоставление базового варианта для рекурсивных преобразователей монад . Его также можно использовать для выполнения базового назначения переменных внутри блока императивного стиля. [л] [ нужна ссылка ]
Любая коллекция с правильным добавлением уже является свободным моноидом, но оказывается, что List
это не единственная коллекция , которая также имеет четко определенное соединение и квалифицируется как монада. Можно даже мутировать List
в эти другие монадические коллекции, просто наложив специальные свойства на добавление : [m] [n]
Как уже упоминалось, чистый код не должен иметь неуправляемых побочных эффектов, но это не мешает программе явно описывать эффекты и управлять ими. Эта идея является центральной для монады IO Haskell , где объект типа IO a
можно рассматривать как описание действия, которое должно быть выполнено в мире, при необходимости предоставляя информацию о мире типа a
. Действие, которое не предоставляет никакой информации о мире, имеет тип IO ()
, «предоставляющий» фиктивное значение ()
. Когда программист привязывает IO
значение к функции, функция вычисляет следующее действие, которое необходимо выполнить, на основе информации о мире, предоставленной предыдущим действием (ввод от пользователей, файлов и т. д.). [23] Самое главное, поскольку значение монады IO может быть привязано только к функции, которая вычисляет другую монаду IO, функция привязки накладывает дисциплину на последовательность действий, при которой результат действия может быть предоставлен только функции. который вычислит следующее действие, которое необходимо выполнить. Это означает, что действия, которые не нужно выполнять, никогда не выполняются, а действия, которые необходимо выполнить, имеют четко определенную последовательность, что решает проблему (IO) действий, которые не являются ссылочно прозрачными .
Например, в Haskell есть несколько функций для работы с более широкой файловой системой , в том числе одна, которая проверяет, существует ли файл, и другая, которая удаляет файл. Их две сигнатуры типа:
doFileExist :: FilePath -> IO Bool RemoveFile :: FilePath -> IO ()
Первый интересуется, действительно ли существует данный файл, и в результате выводит логическое значение внутри IO
монады. Вторая функция, с другой стороны, занимается только воздействием на файловую систему, поэтому контейнер, IO
который она выводит, оказывается пустым.
IO
однако не ограничивается только файловым вводом-выводом; он даже допускает пользовательский ввод-вывод и, наряду с императивным синтаксическим сахаром, может имитировать типичное «Hello, World!» программа :
main :: IO () main = do putStrLn "Привет, мир!" putStrLn "Как тебя зовут, пользователь?" name <- getLine putStrLn ( "Приятно познакомиться, " ++ name ++ "!" )
В удаленном виде это преобразуется в следующий монадический конвейер ( >>
в Haskell это просто вариант связывания , когда важны только монадические эффекты и базовый результат может быть отброшен):
main :: IO () main = putStrLn "Привет, мир!" >> putStrLn "Как тебя зовут, пользователь?" >> getLine >>= ( \ name -> putStrLn ( "Приятно познакомиться, " ++ name ++ "!" )
Другая распространенная ситуация — ведение файла журнала или иной отчет о ходе работы программы. Иногда программисту может потребоваться зарегистрировать еще более конкретные технические данные для последующего профилирования или отладки . Монада Writer может справиться с этими задачами, генерируя вспомогательный вывод, который накапливается шаг за шагом.
Чтобы показать, что шаблон монады не ограничивается преимущественно функциональными языками, в этом примере Writer
монада реализуется в JavaScript . Во-первых, массив (с вложенными хвостами) позволяет построить Writer
тип в виде связанного списка . Базовое выходное значение будет находиться в позиции 0 массива, а позиция 1 будет неявно содержать цепочку вспомогательных примечаний:
const Writer = значение => [ значение , []];
Определить единицу измерения также очень просто:
константная единица = значение => [ значение , []];
Требуется только модульWriter
для определения простых функций, выводящих объекты с отладочными примечаниями:
const Squared = x => [ x * x , [ ` ${ x } был возведен в квадрат.` ]]; const halfed = x => [ x / 2 , [ ` ${ x } было уменьшено вдвое.` ]];
Настоящая монада по-прежнему требует привязки , но для Writer
это означает простое объединение вывода функции в связанный список монады:
const связывание = ( писатель , преобразование ) => { const [ значение , журнал ] = писатель ; const [ результат , обновления ] = преобразование ( значение ); возврат [ результат , журнал . concat ( обновления )]; };
Примеры функций теперь могут быть объединены в цепочку с помощью связывания , но определение версии монадической композиции (называемой pipelog
здесь) позволяет применять эти функции еще более лаконично:
const Pipelog = ( писатель , ... преобразует ) => преобразует . уменьшить ( связать , писатель );
Конечным результатом является четкое разделение задач между пошаговыми вычислениями и их записью в журнал для последующего аудита:
трубопровод ( единица ( 4 ), квадрат , пополам ); // Результирующий объект записи = [8, ['4 было возведено в квадрат.', '16 было уменьшено пополам.']]
Монада среды (также называемая монадой чтения и монадой функции ) позволяет вычислениям зависеть от значений из общей среды. Конструктор типа монады сопоставляет тип T функциям типа E → T , где E — тип общей среды. Функции монады:
Полезны следующие монадические операции:
Операция Ask используется для получения текущего контекста, а операция local выполняет вычисления в измененном подконтексте. Как и в монаде состояния, вычисления в монаде среды могут быть вызваны простым предоставлением значения среды и применением его к экземпляру монады.
Формально значение в монаде окружения эквивалентно функции с дополнительным анонимным аргументом; return иbind эквивалентны комбинаторам K и S соответственно в исчислении комбинаторов SKI .
Монада состояния позволяет программисту прикреплять к вычислению информацию о состоянии любого типа. Учитывая любой тип значения, соответствующий тип в монаде состояния представляет собой функцию, которая принимает состояние, а затем выводит новое состояние (типа s
) вместе с возвращаемым значением (типа t
). Это похоже на монаду среды, за исключением того, что она также возвращает новое состояние и, таким образом, позволяет моделировать изменяемую среду.
Тип Состояние s t = s -> ( t , s )
Обратите внимание, что эта монада принимает параметр типа — тип информации о состоянии. Операции монады определяются следующим образом:
-- «return» возвращает заданное значение без изменения состояния. return x = \ s -> ( x , s ) — «bind» изменяет m так, что оно применяет f к своему результату. m >>= f = \ r -> пусть ( x , s ) = m r in ( f x ) s
Полезные операции состояния включают в себя:
get = \ s -> ( s , s ) — проверим состояние на этом этапе вычислений. put s = \ _ -> ( () , s ) — Заменяем состояние. изменить f = \ s -> ( () , f s ) — обновить состояние
Другая операция применяет монаду состояния к заданному начальному состоянию:
runState :: State s a -> s -> ( a , s ) runState t s = t s
do-блоки в монаде состояния — это последовательности операций, которые могут проверять и обновлять данные состояния.
Неформально, монада состояния типа состояния S отображает тип возвращаемых значений T в функции типа , где S — базовое состояние. Функция возврата и привязки :
С точки зрения теории категорий, монада состояний возникает в результате соединения функтора произведения и экспоненциального функтора, который по определению существует в любой декартовой замкнутой категории .
Монада продолжения [o] с возвращаемым типом R отображает тип T в функции типа . Он используется для моделирования стиля передачи продолжения . Функции возврата и привязки следующие:
Функция вызова с текущим продолжением определяется следующим образом:
Следующий код является псевдокодом.Предположим, у нас есть две функции foo
и bar
с типами
foo : int -> int bar : int -> int
То есть обе функции принимают целое число и возвращают другое целое число. Затем мы можем применить функции последовательно следующим образом:
фу ( бар х )
Где результат является результатом foo
применения к результату bar
применения к x
.
Но предположим, что мы отлаживаем нашу программу и хотим добавить сообщения журналирования в foo
и bar
. Итак, мы меняем типы следующим образом:
foo : int -> int * строка bar : int -> int * строка
Таким образом, обе функции возвращают кортеж с результатом приложения в виде целого числа и сообщение журнала с информацией о примененной функции и всех ранее примененных функциях в виде строки.
К сожалению, это означает, что мы больше не можем составлять foo
и bar
, поскольку их тип ввода int
несовместим с типом вывода int * string
. И хотя мы можем снова добиться компонуемости, изменив типы каждой функции на int * string -> int * string
, это потребует от нас добавления шаблонного кода к каждой функции для извлечения целого числа из кортежа, что будет утомительно по мере увеличения количества таких функций.
Вместо этого давайте определим вспомогательную функцию, которая абстрагирует этот шаблон:
связывание : int * строка -> ( int -> int * строка ) -> int * строка
bind
принимает кортеж целых чисел и строк, затем принимает функцию (например foo
, ), которая отображает целое число в кортеж целых чисел и строк. Его выходные данные представляют собой кортеж целых чисел и строк, который является результатом применения входной функции к целому числу внутри входного целого числа и кортежа строк. Таким образом, нам нужно написать шаблонный код только один раз, чтобы извлечь целое число из кортежа в формате bind
.
Теперь мы восстановили некоторую компоновку. Например:
привязать ( привязать ( x , s ) бар ) foo
Где (x,s)
целое число и строковый кортеж. [п]
Чтобы сделать преимущества еще более очевидными, давайте определим инфиксный оператор как псевдоним для bind
:
( >>= ) : int * строка -> ( int -> int * строка ) -> int * строка
Так что это t >>= f
то же самое, что и bind t f
.
Тогда приведенный выше пример станет:
(( x , s ) >>= bar ) >>= foo
Наконец, было бы неплохо не писать (x, "")
каждый раз, когда мы хотим создать пустое сообщение журнала, где ""
находится пустая строка. Итак, давайте определим новую функцию:
возврат : int -> int * строка
Который обертывается x
в кортеж, описанный выше.
Теперь у нас есть хороший конвейер для регистрации сообщений:
(( return x ) >>= bar ) >>= foo
Это позволяет нам легче регистрировать влияние bar
и foo
на x
.
int * string
обозначает псевдокодированное монадическое значение . [p] bind
и return
аналогичны соответствующим одноименным функциям. Фактически, int * string
, bind
, и return
образуют монаду.
На математическом уровне некоторые монады обладают особенно хорошими свойствами и уникальным образом подходят для решения определенных задач.
Аддитивная монада — это монада, наделенная дополнительным замкнутым ассоциативным бинарным оператором mplus и единичным элементом под mplus , называемым mzero . Монаду Maybe
можно считать аддитивной, с Nothing
mzero и вариацией оператора OR как mplus . List
также является аддитивной монадой, в которой пустой список []
действует как mzero , а оператор конкатенации ++
— как mplus .
Интуитивно, mzero представляет собой монадическую оболочку, не имеющую значения из базового типа, но также считается «нулем» (а не «единицей»), поскольку он действует как поглотитель для связывания , возвращая mzero всякий раз, когда он привязан к монадической функции. Это свойство двустороннее, и функция связывания также вернет mzero , когда какое-либо значение привязано к монадической нулевой функции .
В терминах теории категорий аддитивная монада квалифицируется один раз как моноид по монадическим функциям с привязкой (как и все монады) и снова по монадическим значениям через mplus . [33] [д]
Иногда общее описание монады может оказаться полезным, но ни один простой шаблон не рекомендует ту или иную монаду. Здесь на помощь приходит свободная монада ; как свободный объект в категории монад, он может представлять монадическую структуру без каких-либо конкретных ограничений, помимо самих законов монады. Подобно тому, как свободный моноид объединяет элементы без оценки, свободная монада позволяет связывать вычисления с маркерами для удовлетворения системы типов, но в остальном сама не налагает более глубокой семантики.
Например, если полностью работать с маркерами Just
и Nothing
, Maybe
монада фактически является свободной монадой. List
С другой стороны, монада не является свободной монадой, поскольку она вводит в свое определение дополнительные, конкретные факты о списках (например, Append ) . Последний пример — абстрактная свободная монада:
данные Свободно f a = Чисто a | Свободно ( ж ( Свободно фа ) ) единица :: a -> Свободная единица f a x = Чистый x привязка :: Функтор f => Свободно f a -> ( a -> Свободно f b ) -> Свободно f b привязка ( Pure x ) f = f x привязка ( Свободно x ) f = Свободно ( fmap ( \ y -> привязка у е ) х )
Однако свободные монады не ограничиваются связным списком, как в этом примере, и могут быть построены на основе других структур, таких как деревья .
Намеренное использование свободных монад на первый взгляд может показаться непрактичным, но их формальная природа особенно хорошо подходит для решения синтаксических задач. Свободную монаду можно использовать для отслеживания синтаксиса и типа, оставляя семантику на потом, и в результате она нашла применение в синтаксических анализаторах и интерпретаторах . [34] Другие применяли их и к более динамичным, оперативным задачам, таким как предоставление итераций внутри языка. [35]
Помимо создания монад с дополнительными свойствами, для любой данной монады можно также определить комонаду . Концептуально, если монады представляют собой вычисления, построенные на основе базовых значений, то комонады можно рассматривать как сокращение до значений. Монадический код в некотором смысле не может быть полностью «распакован»; как только значение заключено в монаду, оно остается там в карантине вместе со всеми побочными эффектами (хорошая вещь в чисто функциональном программировании). Однако иногда проблема больше связана с потреблением контекстных данных, которые комонады могут моделировать явно.
Технически комонада — это категориальный двойник монады, что в общих чертах означает, что она будет иметь те же необходимые компоненты, только с обратным направлением сигнатур типов . Исходя из определения связывающей -центрической монады, комонада состоит из:
единица(ва) : WT → T
=>>
), расширяющая цепочку сокращающих функций:(wa =>> f) : (WU, WU → T) → WT [r]
«Продление» и «соединение» также должны удовлетворять двойственным законам монады:
единица ∘ ( (wa =>> f) → wb ) ↔ f(wa) → bва =>> единица ↔ ваwa ( (=>> f(wx = wa)) → wb (=>> g(wy = wb)) → wc ) ↔ ( wa (=>> f(wx = wa)) → wb ) (=>> g(wy = wb)) → wc
Аналогично монадам, комонады также могут быть получены из функторов с использованием двойственного соединения :
дубликат(ва): WT → W (WT)
Однако, хотя такие операции, как расширение , меняются местами, комонады не меняют функции, на которые воздействуют, и, следовательно, комонады по-прежнему являются функторами с картой , а не кофункторами . Альтернативное определение с дубликатом , counit и картой также должно соблюдать свои собственные законы комонады:
((дубликат карты) ∘ дубликат) wa ↔ (дубликат ∘ дубликат) wa ↔ wwwa((единица карты) ∘ дубликат) wa ↔ (единица ∘ дубликат) wa ↔ wa((карта карта φ) ∘ дубликат) wa ↔ (дубликат ∘ (карта φ)) wa ↔ wwb
Как и в случае с монадами, две формы могут быть преобразованы автоматически:
(карта φ) wa ↔ wa =>> (φ ∘ единица) wxдубликат wa ↔ wa =>> wx
wa =>> f(wx) ↔ ((map f) ∘ дубликат) wa
Простым примером является comonad Product , который выводит значения на основе входного значения и общих данных среды. Фактически, Product
комонада — это просто двойник монады Writer
и фактически то же самое, что и Reader
монада (обе обсуждаются ниже). Product
и Reader
различаются только тем, какие сигнатуры функций они принимают, и как они дополняют эти функции путем переноса или развертывания значений.
Менее тривиальный пример — комонада Stream , которую можно использовать для представления потоков данных и присоединения фильтров к входящим сигналам с помощью расширения . Фактически, хотя они и не так популярны, как монады, исследователи обнаружили, что комонады особенно полезны для потоковой обработки и моделирования программирования потоков данных . [36] [37]
Однако из-за их строгих определений нельзя просто перемещать объекты взад и вперед между монадами и комонадами. Будучи еще более высокой абстракцией, стрелки могут включать в себя обе структуры, но поиск более детальных способов объединения монадического и комонадического кода является активной областью исследований. [38] [39]
Альтернативы модельным расчетам:
Связанные концепции дизайна:
Обобщения монад:
bind
, который, если ему задан тип a , который может потерпеть неудачу, и отображение a → b , которое может потерпеть неудачу, дает результат b , который может потерпеть неудачу. (Хаттон, 2016) [7] lift
, а также несколько версий для разного количества параметров, здесь эта деталь игнорируется.Identity
также можно рассматривать как возникающую в результате соединения любого функтора с его обратным.bind
в место , где раньше был только элемент; то есть программист построил дополнение : кортеж , обозначенный в § псевдокода выше.string
integer
(x,s)
int * string
Ссылки на HaskellWiki:
Учебники:
Probability
для цепей Маркова .Интересные случаи: