Здравствуйте, AlexRK, Вы писали:
ARK>Сколько ни видел в интернете трудов наподобие "простое описание монад" — никто не может описать доходчиво. Везде одна и та же хрень: "я понял монады!" и начинается "это тип с операциями bind и return", бла-бла-бла.
Кстати, если не боишься английского, то вот хорошие статьи Бартоша Милевского для С++-ников: http://bartoszmilewski.com/2014/02/26/c17-i-see-a-monad-in-your-future/
Тут на примере std::future разбирается, почему и как это монада и что она собой представляет (и не только монада — заодно поясняется, откуда и зачем появляются аппликативные функторы и прочая). Особенно хорошо поясняются bind и return.
Здравствуйте, AlexRK, Вы писали:
ARK>А как бы это могло выглядеть на С/C++? Нужны были бы новые синтаксические конструкции?
Если интересна просто реализация полноценной монады на C++, то в предыдущей темке я кидал работающие примеры. Так что никакой спец. поддержки в языке в общем то не требуется.
Здравствуйте, AlexRK, Вы писали:
ARK>Я так понимаю, что Haskell героически борется с проблемами, которые сам себе и создал.
Да, именно так.
ARK>А кроме задания последовательности для ввода-вывода и исключений (которые в императивные языки встроены и воспринимаются как нечто само собой разумеющееся) есть какие-то вещи, которые могли бы пригодиться и в императивщине?
На самом деле таких вещей довольно мало. И не потому, что монады такие никчемные, а потому что очень многие вещи, для которых они могли бы подойти, уже реализованы в императивных языках с помощью специальных (менее обобщённых, но зато более удобных) конструкций. Типа исключений и т.п.
В целом я бы сказал, что вводить монаду есть смысл, если нам по условию задачи требуется провести цепочку вычислений вида:
T1 -> M<T2>, T2 -> M<T3>, T3 -> M<T4> и т.д.
Из известных реальных примеров в том же C++ с ходу вспоминается только future. Причём только реализация из Boost'a (в которой есть then, т.к. без этого уже не монада выходит), а не из стандарта языка.
Кстати, подобные конструкции встречаются не только в языках, но и например в различных API. К примеру цепочки в DirectShow вполне можно формализовать и в виде монад. ) Но опять же и в API это весьма редкий случай.
P.S. Да, и вообще по поводу всего этого есть ещё один очень интересный нюанс... В очень многих случаях, когда на практике получается монада, авторы кода на самом деле даже и не думали подобным образом — они просто писали код под задачу. Возможно они даже и не в курсе, что у них там какая-то монада образовалась... )))
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, AlexRK, Вы писали:
ARK>>А как бы это могло выглядеть на С/C++? Нужны были бы новые синтаксические конструкции?
_>Если интересна просто реализация полноценной монады на C++, то в предыдущей темке я кидал работающие примеры. Так что никакой спец. поддержки в языке в общем то не требуется.
Думаю, что монаду (да и много чего еще) можно соорудить на чем угодно.
Только вот хотелось бы, чтобы это выглядело не очень страшно.
Здравствуйте, gandjustas, Вы писали:
G>В C# можно разрулить перегрузками операторов, но это далеко не все случаи покрывает. G>Например такой код: G>
G>from v1 in a
G>from v2 in f(v1)
G>select g(v1,v2)
G>
G>Уже никакими перегрузками не покроешь.
В D без проблем это реализуется. Я кидал работающий пример в предыдущей темке.
G>НО в haskell есть функция lift, которая позволяет "поднять" оператор в монаду. G>Это страшное словосочетание говорит, что имея оператор T `x` V => U и монаду М (тип и две функции) автоматически преобразуется в M<T> `mx` M<V> => M<U>. G>Теоретически в Haskell можно автоматом "поднимать" все операторы для всех монад в текущем контексте, практически мало пользы, ибо самое интересное не описывается арифметическими операторами.
Ну так Хаскель и совсем не идеал. Языки с нормальным метапрограммированием очевидно будут сильнее в таких вещах.
Здравствуйте, AlexRK, Вы писали:
ARK>Думаю, что монаду (да и много чего еще) можно соорудить на чем угодно. ARK>Только вот хотелось бы, чтобы это выглядело не очень страшно.
Ну на плюсах использование выглядит ничуть не страшнее Хаскеля) Только вот смысл использовать здесь монады появляется намного реже. )
D. Mon очень хорошо все расписал.
XC>Сама по себе концепция этой обертки уже очень интересна, без всяких монад. Есть ли какое-то общее определение этой "обертки" Что еще может быть оберткой?
Я точных терминов не знаю. Да, оно связано с теорией категорий. Еще какую-то информацию можно найти по "higher kinded types" и "type constructor". Вот Box — это конструктор типа и при применении к типу T дает в результате Box<T>.
XC>То есть конструктор будущей монады? Берем простое значение и заворачиваем его в бокс. А вот кстати, чтобы взять обратно значение из бокса есть что-нибудь?
Из бокса в общем случае ничего взять нельзя. Он односторонний. В конкретных реализацих (вроде either/option/list) это сделать можно. Но от монад этого не требуется, и не всегда это можно сделать (IO, Async).
Box.return скорее конструктор будущего значения. Монада — это вроде бы сам Box (как конструктор типа). Но для понимания это не важно.
XC>То есть как-бы получается, что для того, чтобы сделать что-то со значением, находящимся в боксе, нужно чтобы у этого бокса был специальный метод, принимающий на вход функцию-обработчик? И это, как я понимаю, замена возможности получения значения, хранящегося в боксе.
Да, именно так.
XC>То есть метод map() или apply() сделали статической функцией, а бокс ей передаем в качестве второго аргумента? ОК. Пока непонятно зачем это.
Мне это нужно с сугубо практической точки зрения. Чтобы варианты с одним аргументом и несколькими друг от друга не очень отличались. Плюс функция вернулась в привычное место — слева от аргумента. То, что D. Mon написал, тоже верно. Плюс в haskell оно достаточно активно используется (там оно в форме lift: (A -> B) -> Box<A> -> Box<B>, именно с таким каррированием) и именно в применениях к функции. В других языках это тоже допустимо, но на практике вроде бы применяется не часто.
XC>На мой неопытный взгляд тут расхождение со статьей на Хабре, на оригинал которой вы ссылались. XC>Там написано что "аппликативный функтор" это когда функция тоже упакована в контейнер, а у вас нечто другое — когда функция применяется для группы "боксов" сразу. Или я чего-то не понял?
Все правильно. Именно с того места мое изложение отходит от стандартной формулировки понятий. Я записываю applicative в "неканонической" форме, при этом эквивалентной исходной. D. Mon одну половинку (выражение функции от многих аргументов через аппликативное применение) показал. Обратное — аналогично. Я бы его как
fn : Box<A -> B>
v : Box<B>
fn <*> v == Box.apply($, fn, v) //или Box.apply(applyFn, [fn, v])
// $ - это в haskell применение функции, так же, как и строка ниже
$ fn x = applyFn(fn, x) = fn(x)
На самом деле ситуация даже интереснее. Каноничная форма записи applicative вызывана системой типов языков, где оно появилось. В haskell ведь все функции от одного аргумента и могут возвращать другие функции. Т.е. "функция от двух аргументов A и B" превращается в "функцию от аргумента A, возвращающую функцию от аргумента B". На практике проблемой не является (а во многих случаях является преимуществом), но накладывает свой отпечаток на разные API.
Подумайте, откуда у вас будет функция внутри Box. Можно сделать return function. Но какой смысл, если у нас уже есть функтор и можно применить функцию с значению? Т.е. вместо (return fn) <*> value можно написать сразу fn <$> value (fmap fn value или даже fn `fmap` value, это все одно и то же). Во многих случаях функции попадают внутрь Box в результате применения к ним функтора или другого аппликативного применения. Т.е. была "чистая" функция возвращающая функцию (A -> B -> C). Ее применили к значению внутри бокса Box<A> и только после этого получили функцию внутри бокса Box<B -> C>. И вот потом уже этот результат (бокс с функцией) аппликативно применяется к боксу со следующим значением.
В очень многих случаях вы увидите
fn1 : A -> B -> C
fn2 : A -> B -> C -> D
v1 : Box<A>
v2 : Box<B>
v3 : Box<C>
r1 : Box<C> = fn1 <$> v1 <*> v2 == Box.apply(Box.map(fn1, v1), v2)
r2 : Box<D> = fn2 <$> v1 <*> v2 <*> v3 ==
Box.apply(Box.apply(Box.map(fn1, v), v2), v3)
Если язык поддерживает манипуляции с функциями от многих аргументов (javascript, lisp) это можно свернуть в один вызов "обобщенной" функции. Формализмы получаются разные, но в результате эквивалентные друг другу. А вот основная идея и практические применения у них общие.
Здравствуйте, jazzer, Вы писали:
J>Кстати, если не боишься английского, то вот хорошие статьи Бартоша Милевского для С++-ников:
Не люблю я этого Бартоша. По-моему, он просто несёт около-программистский бред. Такое ощущение, что в C++ он не очень силён, что он пытается скомпенсировать примерчиками из Хаскеля (достаточно примитивными, должен сказать). В результате получается полная муть.
Здравствуйте, uncommon, Вы писали:
U>Здравствуйте, jazzer, Вы писали:
J>>Кстати, если не боишься английского, то вот хорошие статьи Бартоша Милевского для С++-ников:
U>Не люблю я этого Бартоша. По-моему, он просто несёт около-программистский бред. Такое ощущение, что в C++ он не очень силён, что он пытается скомпенсировать примерчиками из Хаскеля (достаточно примитивными, должен сказать). В результате получается полная муть.
Это ты прочитал статьи, ссылки на которые я дал выше, я правильно понимаю?
Здравствуйте, alex_public, Вы писали:
G>>Теоретически в Haskell можно автоматом "поднимать" все операторы для всех монад в текущем контексте, практически мало пользы, ибо самое интересное не описывается арифметическими операторами.
_>Ну так Хаскель и совсем не идеал. Языки с нормальным метапрограммированием очевидно будут сильнее в таких вещах.
Во-первых, очень мало языков с нормальным метапрограммированием. Во-вторых, метапрограммироание, даже самое простое, дает неустранимую проблему — метакод никогда не бывает так же прозрачен, как и обычный код. Чем мощнее метапрограммирование, тем ниже прозрачность кода. То есть,
return f(g(h(x)))
Вот такая строчка уже ни о чем не говорит. ты просто не знаешь, чего ждать от выражения, потому что самое главное будет "где-то рядом", "где-то здесь", "где-то там"
Здравствуйте, AlexRK, Вы писали:
ARK>Может кто-нибудь описать простым языком — что такое монады и зачем они нужны?
Если коротко, то монада — это любая обёртка данных, подобная списку. Возможно — списку из одного элемента (IO), возможно, из нуля-или-одного (Maybe).
Если ты знаком с нотацией set comprehension, то всё окажется довольно просто.
1. Списочная функция берёт аргументы и возвращает список результатов. f(x) = [...]
2. Если каждый аргумент взят из своего списка, то мы получаем список списков: [ f(x) for x in Xs ] = [ [...], [...], ..... ]; этот список автоматически расплющивается через конкатенацию
Fs = [...]++[...]++[...]
3. Композиция источников f(g(x)): flatten([ g(y) for x in Xs for y in f(x) ])
У всякой монады есть тривиальный конструктор, обёртка одного значения: [x]
Зачастую, у монады есть нулевой элемент, подобный пустому множеству: []. Кстати, у IO его нет.
Если функция f(x) вернула пустой список, то забег по for y выродится, и нечего будет подавать на вход g(y), и соответственно, нечего будет расплющивать.
Нулевой элемент используется, таким образом, для мгновенного завершения вычислений.
Это Nothing у Maybe, это Left smth у Either, ну и собственно пустой список.
Монада, помимо списка результатов, может содержать что-то полезное сбоку.
Тогда процедура вызова чуть усложняется:
def f(token, x):
.....
return token2, [a,b,c]
def g(token, y):
.....
return token3, [d,e,f]
def call_augmented(g, txs):
t,xs = txs
zs = []
for x in xs:
t,ys = g(t,x)
zs += ys # сразу же и сплющиваемreturn t,zs
txs = token0,[x1,x2,x3]
tys = call_augmented(f, txs)
tzs = call_augmented(g, tys)
Функция может быть прозрачной для элементов, и заниматься подправлением этой побочной информации
def modify_token(token, x):
return token+1, [x]
def extract_token(token, x):
return token, [token]
def check_token(token, x):
if token is bad :
return token, []
else:
return token, [x]
На питоне, кстати, с его генераторами, сопрограммами и конструкторами списков писать монадический код — полтора удовольствия.
Там и сахар присобачить можно, и карринг. Только do-нотация немножко ногами пишется.
ARK>Или монады это такая сложная концепция, что описать ее доступным языком невозможно в принципе?
Да авторы хаскелла нашли удачный базис и воспользовались. А вот другие сущности из теорката — стрелки, — чего-то не выстрелили. Сейчас вроде модно использовать аппликативные функторы.
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 в 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>Казалось бы, не такая и большая разница между двумя API. На самом деле очень большая, потому что мы будем обобщать применение функций дальше. Итак, следующая секция:
M>
Applicatives
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>Использовать 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 операции на каждый класс). В результате чего места для ошибок практически нет (не надо вручную все компоненты синхронизировать, например). Ну и вид программы более декларативный. Я объявляю те же компоненты интерфейса (и текущий вид) как функцию от входных данных и состояний, а не как набор методов по копированию всего подряд из модели в вид и обратно.
Меня больше всего смущает используемый синтаксис. Хотелось, бы видеть, что-то членораздельное. Пока что — вырви глаз.
Очередной раз пытаюсь понять, но видя такой синтаксис понимаю, что я никогда их в таком виде не буду использовать, просто из-за синтаксиса.
Здравствуйте, AlexRK, Вы писали:
ARK>Или монады это такая сложная концепция, что описать ее доступным языком невозможно в принципе?
Если вы знакомы с ООП паттерном Interpreter, то монады можно объяснить либо как частичное-альтернативное решение тех же самых задач, либо вообще как очень специфический случай реализации самого паттерна.