Re[2]: Монады
От: omgOnoz  
Дата: 18.11.14 09:49
Оценка:
Здравствуйте, maxkar, Вы писали:

  Скрытый текст
M>Здравствуйте, AlexRK, Вы писали:

ARK>>Может кто-нибудь описать простым языком — что такое монады и зачем они нужны?


M>Я могу! И отвечу еще на ряд вопросов, заданных в ветке.


M>Монады в классическом виде такие страшные, потому что помимо основной идеи еще идет борьба с системой типов языка. Концепция на самом деле более универсальная и удобная.


M>Теперь по порядку. Введение в весь стек с монадами в картинках. Полезно, если любите графическое представление. Плюс до функторов (включительно) я буду говорить примерно о том же.


M>

Значения, функции и Box

M>С примитивными (и не только) значениями все вроде бы понятно. Это обычные значения. Функции — тоже вполне классические функции, включая функции от многих аргументов:
M>
M>f1 : T => R
M>f2 : (T1, T2, T3, ..., TN) => R
M>


M>Для введения дальнейших понятий нам нужен еще какой-нибудь контейнер, в который можно "положить" значение любого типа. Пусть это будет Box<T>. Как у него семантика — зависит от реализации. Это может быть Option, может быть Either, может быть асинхронное выполнение, observable, collection, etc... Важно только то, что это некая "обертка" поверх типа.


M>Здесь же можно отметить, что у Box должен быть метод "упаковки" простого значения в контейнер. Например, так:

M>
M>Box.box : T => Box<T>
M>//применение
M>val boxed : Box<Int> = Box.box(3)
M>

M>Это является аналогом монадного return.

M>

Functor

M>Просто складывать значения в Box не интересно (можно складывать Box в Box, но это тоже быстро надоест). Хочется сделать что-нибудь с Box<T>. Например, у нас Box — асинхронная задача. И хочется что-нибудь запустить после завершения результата. Другими словами, применить функцию к результату операции. Очевидно, что применение "чистой функции" к асинхронной операции может применить эту функцию не сразу, а по завершению операции. Т.е "природа" Box сохраняется.

M>Можно сделать применение такой функции, например, так:

M>
M>class Box<T> {
M>  // какая-то реализация семантики Box
  
M>  //Применение функции "внутри" box 
M>  def map<R>(fn : T => R) : Box<R>
M>}

M>// Применение к асинхронной операции
M>val operation : Async<Image> = loadImage(...);
M>operation.map(image => alert("Она загрузилась!"));
M>

M>Это может быть применение функции к результату асинхронной операции, применение функции к элементу коллекции/option/either и т.д. в зависимости от семантики Box. Вот эта вот функция map называется функтором.

M>Если вы внимательно посмотрите, это очень напоминает Promise. И это очень неудачный вариант предоставления функтора! Например, почти эквивалентно будет

M>
M>class Box<T> {
M>  static def<T, R> map(fn : T => R, box : Box<T>) : Box<R>
M>}

M>//Применение к асинхронной операции:
M>val operation : Async<Image> = loadImage(...);
M>Async.map(image => alert("Оно загрузилось"), operation);

M>//Или более реалистичный пример, с дополнительными функциями
M>val operation : Async<Image> = loadImage(...);
M>Async.apply(showImage, operation);

M>def showImage(image : Image) : void {
M>  //....
M>}
M>

M>Казалось бы, не такая и большая разница между двумя API. На самом деле очень большая, потому что мы будем обобщать применение функций дальше. Итак, следующая секция:

M>

Applicatives

M>Усложним задачу. Теперь нужно грузить не один ресурс, а несколько. Например, картинку с нашего сервера и какие-то данные с сервера партнеров. И после этого отображать окно интерфейса. Т.е. у нас уже есть
M>
M>val imageOp : Async<Image> = loadImage(...);
M>val partnerData : Async<SpamData> = loadPartnerData(...);

M>def showUI(image : Image, data : SpamData) : void {
M>  //...
M>}
M>

M>И теперь нам нужно бы связать операции с целевой функцией. Но ведь в предыдущей секции мы что-то подобное уже делали! Можно попробовать сделать точно так же:
M>
M>Box.map(showUI, imageOp, partnerData);
M>

M>Т.е. нам нужно обобщить применение функции с одного Box на несколько. Если система типов не сильно мешает, можно сделать и прямолинейно:
M>
M>class Box<T> {
M>  static def map<T1, T2, T3, ..., TN, R>(
M>    fn : (T1, T2, T3, ..., TN) => R,
M>    a1 : Box<T1>, a2 : Box<T2>, a3 : Box<T3>, ..., an : Box<TN>) : Box<R>
M>}

M>// Вполне реальный для JS API:
M>// Такими могли бы быть Promise! 
M>// И Promise достаточно легко допиливаются до этого состояния.
M>var imageOp = loadImage(...);
M>var spamData = loadPartnerData(...);
M>Aysnc.apply(showUI, [imageOp, spamData]);

M>// Сравните с обычным (синхронным) применением:
M>var image = getSomeImage();
M>var spamData = getSomeData();
M>showUI(image, spamData);
M>

M>Вот так получается, если система типов не мешает. Мы обобщили "функтор" на несколько аргументов и получили нечто. Вот именно для получения этого нечто и сделан Applicative в haskell и аналогичных языках! В первую очередь для обобщения применения функции к нескольким Box'ам в рамках системы типов. И на практике именно так аппликативы и применяются:
M>
M>fn <$> arg1 <*> arg2 <*> arg3
M>


M>В то же время это очень похоже на обычное применение функции к аргументам. Только теперь функция применяется не к самим аргументам, а к Box<arg>.


M>

Монады

M>Пойдем еще дальше. Нам нужно грузить некоторые данные в зависимости от уже полученных данных. Например, получив данные от партнера мы хотим еще загрузить картинку. Т.е. у нас есть функция, которая по входным данным запускает некоторый процесс и мы хотим применить ее к асинхронно загружаемым данным.
M>
M>var partnerData : Async<Int> = loadPartnerData(...);
M>var otherPartnerData : Async<String> = loadPartnerData(...);
M>var evenMoreData : Async<String> = loadEvenMoreData(...);

M>def loadDependentData(d1 : Int, d2 : String, d3 : String) : Async<Image> {
M>  final String imageUrl = d2 + d1 + d3;
M>  return loadImage(imageUrl);
M>}
M>

M>Использовать Async.apply не пройдет по типам, у нас будет Async<Async<Image>>. Но мы пока изобратаем API. Так что давайте добавим еще один метод:

M>
M>class Box<T> {
M>  static def mmap<T1, T2, T3, ..., TN, R>(
M>    fn : (T1, T2, T3, ..., TN) => Box<R>,
M>    a1 : Box<T1>, a2 : Box<T2>, a3 : Box<T3>, ..., an : Box<TN>) : Box<R>
M>}

M>// Применение!
M>// И это тоже стоило бы сделать в Promise, но...
M>var partnerImage : Box<Image> = Async.mapply(loadDependentData, partnerData, otherPartnerData, evenMoreData);
M>Async.apply(showUI, partnerImage);
M>

M>Вот этот вот mmap + Box.box из первого пункта и дают монаду . Да, с формальной точки зрения это не монада (у монады другой формализм). А вот решаемая задача — именно эта. И на самом деле классичесаая монада эквивалентна вот этой "обобщенной функции" mmap. Они достаточно просто выражаются друг через друга. Зато с практической точки зрения мой вариант понятнее. И заодно показывает, как примерно монады применяются. На самом деле в таком "аппликативном стиле" они удобны даже в императивных языках (в примерах показан Async, именно с таким API я себе делал его и успешно использовал).

M>

Всё вместе

M>Сводная табличка идей (cheat sheet):
M>
M>// function application
M>var a : T1 = ..., b : T2 = ..., c : T3 = ...;
M>def f(x1 : T1, x2 : T2, x3 : T3) : R
M>var r : R = f(a, b, c);

M>// Functor
M>// Только для одноаргументных функций
M>var a : Box<T1> = ...;
M>def f(x1 : T1) : R
M>var r : Box<R> = Box.apply(f, a);

M>// "Applicative"
M>var a : Box<T1> = ..., b : Box<T2> = ..., c : Box<T3> = ...;
M>def f(x1 : T1, x2 : T2, x3 : T3) : R
M>var r : Box<R> = Box.apply(f, a, b, c);

M>// "Monad"
M>var a : Box<T1> = ..., b : Box<T2> = ..., c : Box<T3> = ...;
M>def f(x1 : T1, x2 : T2, x3 : T3) : Box<R>
M>var r : Box<R> = Box.mapply(f, a, b, c);
M>


M>

Немного практики

M>Теперь о практической части. Да, я использовал и использую монады в императивщине. Например, в виде API из cheat sheet оно использовалось в AS3 (язык позволяет такие фокусы) для реактивного программирования и асинхронных операций. Вполне успешно и удобно. Сейчас есть "посмореть" библиотечка аппликативного реактивного программирования для scala. Вот там в силу языка аппликативы и монады гораздо больше похожи на классичесчкий вариант. Есть даже небольшой пример использования библиотечки в действии (не дописан, но смотреть можно, там уже база есть). Искать использование по ":<" и ":>" (без кавычек).

M>Плюсы этих "монад" для практики. Большую часть приложения все еще можно изобразить в чистых функциях. Их удобно писать и тестировать. Какая-то небольшая часть приложения работает с "грязными" данными (в виде асинхронных операций, текущего изменяемого состояния представленного в виде Behavior, etc...). Применение функций в этих классах грязных данных тоже оттестировано (functor, applicative, monad это 4 операции на каждый класс). В результате чего места для ошибок практически нет (не надо вручную все компоненты синхронизировать, например). Ну и вид программы более декларативный. Я объявляю те же компоненты интерфейса (и текущий вид) как функцию от входных данных и состояний, а не как набор методов по копированию всего подряд из модели в вид и обратно.


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

Очередной раз пытаюсь понять, но видя такой синтаксис понимаю, что я никогда их в таком виде не буду использовать, просто из-за синтаксиса.
Отредактировано 18.11.2014 9:53 omgOnoz . Предыдущая версия . Еще …
Отредактировано 18.11.2014 9:53 omgOnoz . Предыдущая версия .
Отредактировано 18.11.2014 9:51 omgOnoz . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.