Здравствуйте, AlexRK, Вы писали:
ARK>>>Интересно, можно ли этого достичь без метапрограммирования? I>>Нужна или поддержка монад в языке, или метапрограммирование, или, на худой конец, динамическая типизация.
ARK>А как, например, может выглядеть поддержка монад в языке? Особенно императивном.
yield это монада. аналогично монада await. можно более общий подход взять — computational expressions
В кратце — кое какие ключевые слова языка можно "перегрузить", почти как операторы:
Вот это поддержка сахаром
let maybe = new MaybeBuilder()
let sugared =
maybe {
let x = 12
let! y = Some 11
let! z = Some 30
return x + y + z
}
Это реальный выхлоп
let maybe = new MaybeBuilder()
let desugared =
maybe.Delay(fun () ->
let x = 12
maybe.Bind(Some 11, fun y ->
maybe.Bind(Some 30, fun z ->
maybe.Return(x + y + z)
)
)
)
var middleIsRegex = from lp in lparen
from rx in regex
from rp in rparen
select rx;
var repeatedStar = from s in _.Repeat0(star)
select s.HasValue ? s.Value[0] :Blank.New();
var repetition = from rx in _.Or(middleIsRegex, primitive)
from rs in repeatedStar
select rs is AST.Blank ? rx : Repetition.New(rx);
Вот тот же фрагмент, монады но на функциях вместо query comprehension
var middleIsRegex = _.Sequence(lparen, regex, rparen).Select(x => string.Concat(x));
var regexOrPrimitive = _.Or(middleIsRegex, primitive);
var repeatedStar = _.Repeat0(star).Select(toString);
var repetition = _.Sequence(regexOrPrimitive, repeatedStar).Select(x => string.Concat(x));
Здесь очевидно комбинаторов нужно больше, т.е. кодить саму либу труднее, а в более сложных случаях и на вызывающей стороне придется приседать с лямбдами.
В качестве бонуса query comprehension дает как бэ паттерн матчинг. Одно плохо — сходу непонятно, что унутре query. Для незнакомого человека функции более понятны.
Здравствуйте, AlexRK, Вы писали:
I>>А ты таки попробуй Без этого сложновато рассуждать про монады.
ARK>Э... а что именно попробовать? Что у нас на входе, и что мы хотим на выходе? ARK>Я так понимаю, что речь о каком-то таком цикле ARK>
ARK>foreach (var item in arr)
ARK>{
ARK> use(item);
ARK>}
ARK>
ARK>?
Представь, что функция use асинхронная, но надо сохранить фактический порядок.
Здравствуйте, gandjustas, Вы писали:
G>В том то и дело, что это субъективно. G>Это как водить машину. Если ты всю жизнь ездил, то сесть за руль даже камаза не составит большого труда. Если нет, то будет казатся очень сложным. G>Люди, которые всю жизнь пишут рекурсию считают циклы сложными, те кто пишут циклы — считают сложной рекурсию.
Очень интересно, особенно если посмотреть как дети возраста 6-7 лет осваивают инструкции и разные итерационные по своей природе вещи.
Циклам можно научить примерно с 3го класса. Рекурсии — не раньше, чем люди внятно функции освоят, т.е. примерно на 5 лет позже.
Здравствуйте, Ikemefula, Вы писали:
I>yield это монада. аналогично монада await. можно более общий подход взять — computational expressions I>В кратце — кое какие ключевые слова языка можно "перегрузить", почти как операторы:
I>Вот это поддержка сахаром I>
I>let maybe = new MaybeBuilder()
I>let sugared =
I> maybe {
I> let x = 12
I> let! y = Some 11
I> let! z = Some 30
I> return x + y + z
I> }
I>
I>Это реальный выхлоп I>
I>let maybe = new MaybeBuilder()
I>let desugared =
I> maybe.Delay(fun () ->
I> let x = 12
I> maybe.Bind(Some 11, fun y ->
I> maybe.Bind(Some 30, fun z ->
I> maybe.Return(x + y + z)
I> )
I> )
I> )
I>
О, спасибо. Вот это мне уже понятнее.
Мало того, я даже кажется стал понимать, что к чему.
Бинд это типа для насаживания монад друг на друга, чтобы получить цепочку, а ретурн — вызывается, когда эту всю цепь надо вычислить.
Здравствуйте, AlexRK, Вы писали:
ARK>О, спасибо. Вот это мне уже понятнее. ARK>Мало того, я даже кажется стал понимать, что к чему. ARK>Бинд это типа для насаживания монад друг на друга, чтобы получить цепочку, а ретурн — вызывается, когда эту всю цепь надо вычислить.
Здравствуйте, Ikemefula, Вы писали:
ARK>>Э... а что именно попробовать? Что у нас на входе, и что мы хотим на выходе? ARK>>Я так понимаю, что речь о каком-то таком цикле ARK>>
ARK>>foreach (var item in arr)
ARK>>{
ARK>> use(item);
ARK>>}
ARK>>
ARK>>?
I>Представь, что функция use асинхронная, но надо сохранить фактический порядок.
Наверное, в этом примере надо еще что-то дополнить, иначе все равно непонятно. Если "use" у нас внутри содержит побочные эффекты, то ее и асинхронно запускать нельзя. А если не содержит, то зачем нам порядок?
Здравствуйте, AlexRK, Вы писали:
ARK>>>Э... а что именно попробовать? Что у нас на входе, и что мы хотим на выходе? ARK>>>Я так понимаю, что речь о каком-то таком цикле ARK>>>
ARK>>>foreach (var item in arr)
ARK>>>{
ARK>>> use(item);
ARK>>>}
ARK>>>
ARK>>>?
I>>Представь, что функция use асинхронная, но надо сохранить фактический порядок.
ARK>Наверное, в этом примере надо еще что-то дополнить, иначе все равно непонятно. Если "use" у нас внутри содержит побочные эффекты, то ее и асинхронно запускать нельзя. А если не содержит, то зачем нам порядок?
Содержит, конечно, побочные эффекты. Она сама может быть асинхронной по своей природе, кажем IOCP или к таймеру привязана.
Собтсвенно потому и надо явно ограничивать последовательность, если функция асинхронная и с побоычными эффектами
Предположим, use раз она асинхронная, то принимает колбек для завершения:
Здравствуйте, AlexRK, Вы писали:
ARK>Может кто-нибудь описать простым языком — что такое монады и зачем они нужны?
Я могу! И отвечу еще на ряд вопросов, заданных в ветке.
Монады в классическом виде такие страшные, потому что помимо основной идеи еще идет борьба с системой типов языка. Концепция на самом деле более универсальная и удобная.
Теперь по порядку. Введение в весь стек с монадами в картинках. Полезно, если любите графическое представление. Плюс до функторов (включительно) я буду говорить примерно о том же.
Значения, функции и Box
С примитивными (и не только) значениями все вроде бы понятно. Это обычные значения. Функции — тоже вполне классические функции, включая функции от многих аргументов:
f1 : T => R
f2 : (T1, T2, T3, ..., TN) => R
Для введения дальнейших понятий нам нужен еще какой-нибудь контейнер, в который можно "положить" значение любого типа. Пусть это будет Box<T>. Как у него семантика — зависит от реализации. Это может быть Option, может быть Either, может быть асинхронное выполнение, observable, collection, etc... Важно только то, что это некая "обертка" поверх типа.
Здесь же можно отметить, что у Box должен быть метод "упаковки" простого значения в контейнер. Например, так:
Box.box : T => Box<T>
//применение
val boxed : Box<Int> = Box.box(3)
Это является аналогом монадного return.
Functor
Просто складывать значения в Box не интересно (можно складывать Box в Box, но это тоже быстро надоест). Хочется сделать что-нибудь с Box<T>. Например, у нас Box — асинхронная задача. И хочется что-нибудь запустить после завершения результата. Другими словами, применить функцию к результату операции. Очевидно, что применение "чистой функции" к асинхронной операции может применить эту функцию не сразу, а по завершению операции. Т.е "природа" Box сохраняется.
Можно сделать применение такой функции, например, так:
class Box<T> {
// какая-то реализация семантики Box
//Применение функции "внутри" box
def map<R>(fn : T => R) : Box<R>
}
// Применение к асинхронной операции
val operation : Async<Image> = loadImage(...);
operation.map(image => alert("Она загрузилась!"));
Это может быть применение функции к результату асинхронной операции, применение функции к элементу коллекции/option/either и т.д. в зависимости от семантики Box. Вот эта вот функция map называется функтором.
Если вы внимательно посмотрите, это очень напоминает Promise. И это очень неудачный вариант предоставления функтора! Например, почти эквивалентно будет
class Box<T> {
static def<T, R> map(fn : T => R, box : Box<T>) : Box<R>
}
//Применение к асинхронной операции:
val operation : Async<Image> = loadImage(...);
Async.map(image => alert("Оно загрузилось"), operation);
//Или более реалистичный пример, с дополнительными функциями
val operation : Async<Image> = loadImage(...);
Async.apply(showImage, operation);
def showImage(image : Image) : void {
//....
}
Казалось бы, не такая и большая разница между двумя API. На самом деле очень большая, потому что мы будем обобщать применение функций дальше. Итак, следующая секция:
Applicatives
Усложним задачу. Теперь нужно грузить не один ресурс, а несколько. Например, картинку с нашего сервера и какие-то данные с сервера партнеров. И после этого отображать окно интерфейса. Т.е. у нас уже есть
val imageOp : Async<Image> = loadImage(...);
val partnerData : Async<SpamData> = loadPartnerData(...);
def showUI(image : Image, data : SpamData) : void {
//...
}
И теперь нам нужно бы связать операции с целевой функцией. Но ведь в предыдущей секции мы что-то подобное уже делали! Можно попробовать сделать точно так же:
Box.map(showUI, imageOp, partnerData);
Т.е. нам нужно обобщить применение функции с одного Box на несколько. Если система типов не сильно мешает, можно сделать и прямолинейно:
class Box<T> {
static def map<T1, T2, T3, ..., TN, R>(
fn : (T1, T2, T3, ..., TN) => R,
a1 : Box<T1>, a2 : Box<T2>, a3 : Box<T3>, ..., an : Box<TN>) : Box<R>
}
// Вполне реальный для JS API:
// Такими могли бы быть Promise!
// И Promise достаточно легко допиливаются до этого состояния.
var imageOp = loadImage(...);
var spamData = loadPartnerData(...);
Aysnc.apply(showUI, [imageOp, spamData]);
// Сравните с обычным (синхронным) применением:
var image = getSomeImage();
var spamData = getSomeData();
showUI(image, spamData);
Вот так получается, если система типов не мешает. Мы обобщили "функтор" на несколько аргументов и получили нечто. Вот именно для получения этого нечто и сделан Applicative в haskell и аналогичных языках! В первую очередь для обобщения применения функции к нескольким Box'ам в рамках системы типов. И на практике именно так аппликативы и применяются:
fn <$> arg1 <*> arg2 <*> arg3
В то же время это очень похоже на обычное применение функции к аргументам. Только теперь функция применяется не к самим аргументам, а к Box<arg>.
Монады
Пойдем еще дальше. Нам нужно грузить некоторые данные в зависимости от уже полученных данных. Например, получив данные от партнера мы хотим еще загрузить картинку. Т.е. у нас есть функция, которая по входным данным запускает некоторый процесс и мы хотим применить ее к асинхронно загружаемым данным.
Использовать Async.apply не пройдет по типам, у нас будет Async<Async<Image>>. Но мы пока изобратаем API. Так что давайте добавим еще один метод:
class Box<T> {
static def mmap<T1, T2, T3, ..., TN, R>(
fn : (T1, T2, T3, ..., TN) => Box<R>,
a1 : Box<T1>, a2 : Box<T2>, a3 : Box<T3>, ..., an : Box<TN>) : Box<R>
}
// Применение!
// И это тоже стоило бы сделать в Promise, но...
var partnerImage : Box<Image> = Async.mapply(loadDependentData, partnerData, otherPartnerData, evenMoreData);
Async.apply(showUI, partnerImage);
Вот этот вот mmap + Box.box из первого пункта и дают монаду . Да, с формальной точки зрения это не монада (у монады другой формализм). А вот решаемая задача — именно эта. И на самом деле классичесаая монада эквивалентна вот этой "обобщенной функции" mmap. Они достаточно просто выражаются друг через друга. Зато с практической точки зрения мой вариант понятнее. И заодно показывает, как примерно монады применяются. На самом деле в таком "аппликативном стиле" они удобны даже в императивных языках (в примерах показан Async, именно с таким API я себе делал его и успешно использовал).
Всё вместе
Сводная табличка идей (cheat sheet):
// function application
var a : T1 = ..., b : T2 = ..., c : T3 = ...;
def f(x1 : T1, x2 : T2, x3 : T3) : R
var r : R = f(a, b, c);
// Functor
// Только для одноаргументных функций
var a : Box<T1> = ...;
def f(x1 : T1) : R
var r : Box<R> = Box.apply(f, a);
// "Applicative"
var a : Box<T1> = ..., b : Box<T2> = ..., c : Box<T3> = ...;
def f(x1 : T1, x2 : T2, x3 : T3) : R
var r : Box<R> = Box.apply(f, a, b, c);
// "Monad"
var a : Box<T1> = ..., b : Box<T2> = ..., c : Box<T3> = ...;
def f(x1 : T1, x2 : T2, x3 : T3) : Box<R>
var r : Box<R> = Box.mapply(f, a, b, c);
Немного практики
Теперь о практической части. Да, я использовал и использую монады в императивщине. Например, в виде API из cheat sheet оно использовалось в AS3 (язык позволяет такие фокусы) для реактивного программирования и асинхронных операций. Вполне успешно и удобно. Сейчас есть "посмореть" библиотечка аппликативного реактивного программирования для scala. Вот там в силу языка аппликативы и монады гораздо больше похожи на классичесчкий вариант. Есть даже небольшой пример использования библиотечки в действии (не дописан, но смотреть можно, там уже база есть). Искать использование по ":<" и ":>" (без кавычек).
Плюсы этих "монад" для практики. Большую часть приложения все еще можно изобразить в чистых функциях. Их удобно писать и тестировать. Какая-то небольшая часть приложения работает с "грязными" данными (в виде асинхронных операций, текущего изменяемого состояния представленного в виде Behavior, etc...). Применение функций в этих классах грязных данных тоже оттестировано (functor, applicative, monad это 4 операции на каждый класс). В результате чего места для ошибок практически нет (не надо вручную все компоненты синхронизировать, например). Ну и вид программы более декларативный. Я объявляю те же компоненты интерфейса (и текущий вид) как функцию от входных данных и состояний, а не как набор методов по копированию всего подряд из модели в вид и обратно.
Здравствуйте, maxkar, Вы писали:
M>Для введения дальнейших понятий нам нужен еще какой-нибудь контейнер, в который можно "положить" значение любого типа. Пусть это будет Box<T>. Как у него семантика — зависит от реализации. Это может быть Option, может быть Either, может быть асинхронное выполнение, observable, collection, etc... Важно только то, что это некая "обертка" поверх типа.
Вообще интересно... "Box" — это некая обертка вокруг значения. Это может быть дополнительная информация (option, either). Или несколько равноправных значений (list). Или вообще функция, которая это значение рассчитывает (асинхронное выполнение). Все это очень разные понятия.
Сама по себе концепция этой обертки уже очень интересна, без всяких монад. Есть ли какое-то общее определение этой "обертки" Что еще может быть оберткой?
M>Здесь же можно отметить, что у Box должен быть метод "упаковки" простого значения в контейнер. Например, так: M>
То есть конструктор будущей монады? Берем простое значение и заворачиваем его в бокс. А вот кстати, чтобы взять обратно значение из бокса есть что-нибудь?
M>Просто складывать значения в Box не интересно (можно складывать Box в Box, но это тоже быстро надоест). Хочется сделать что-нибудь с Box<T>. Например, у нас Box — асинхронная задача. И хочется что-нибудь запустить после завершения результата. Другими словами, применить функцию к результату операции. Очевидно, что применение "чистой функции" к асинхронной операции может применить эту функцию не сразу, а по завершению операции. Т.е "природа" Box сохраняется.
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);
То есть метод map() или apply() сделали статической функцией, а бокс ей передаем в качестве второго аргумента? ОК. Пока непонятно зачем это.
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>
На мой неопытный взгляд тут расхождение со статьей на Хабре, на оригинал которой вы ссылались.
Там написано что "аппликативный функтор" это когда функция тоже упакована в контейнер, а у вас нечто другое — когда функция применяется для группы "боксов" сразу. Или я чего-то не понял?
В общем если не сложно прокомментируйте мои комментарии, хотя-бы да/нет
В любом случае спасибо, объяснение супер, завтра продолжу разбираться с остальной частью.
Здравствуйте, AlexRK, Вы писали:
ARK>У меня пока складывается впечатление, что монады могут быть заменены кодогенерацией — причем с лучшим результатом, без всяких нотаций и т.п.
У меня пока складывается впечатление, что классы могут быть заменены кодогенерацией — причём с лучшим результатом, без всяких нотаций и т.п.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Ikemefula, Вы писали: I>Циклам можно научить примерно с 3го класса. Рекурсии — не раньше, чем люди внятно функции освоят, т.е. примерно на 5 лет позже.
Странно. А у нас ханойские башни дети в садике перекладывали. Это примерно на 4 года раньше, чем третий класс.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, AlexRK, Вы писали:
ARK>>У меня пока складывается впечатление, что монады могут быть заменены кодогенерацией — причем с лучшим результатом, без всяких нотаций и т.п. S>У меня пока складывается впечатление, что классы могут быть заменены кодогенерацией — причём с лучшим результатом, без всяких нотаций и т.п.
Можете продемонстрировать, что будет достигнуто путем такой замены?
автоматической проверки errno и прерывания исполнения функции, если что не так — стандартная сишная задача. Если бы были монады — можно было бы спрятать это внутрь оной и код стал бы на порядок чище.
Так а чем это в таком случае принципиально отличается от продвинутых макросов? Подозреваю в чем-то вроде немерла с использованием макросов можно сделать тоже самое.
Тут ниже приводят в пример linq, подозреваю он тоже реализуется на макросах...
Здравствуйте, Sinclair, Вы писали:
I>>Циклам можно научить примерно с 3го класса. Рекурсии — не раньше, чем люди внятно функции освоят, т.е. примерно на 5 лет позже. S>Странно. А у нас ханойские башни дети в садике перекладывали. Это примерно на 4 года раньше, чем третий класс.
Ханойские башни да в садике — не верю. Здесь нужны внятные комбинаторные способности. Пудозреваю, ты говоришь просто про башни из колечек.
Рекурсия == абстркация. При чем очень высокого уровня. Внятные абстрации у детей появляются только лет после 12. В детском саду они только-только начинают появляться.
Если ты не согласен, можно проверить вместе — по скайпу например моим детям, они как раз такого возраста, как надо, а по успеваемости выше среднего в группе.
Дам 100$ если ты хотя бы одному объяснишь _рекурсию_.
Что касается циклов — примерно в 3м классе дети учатся выполнять инструкции вида "1 сделать 2 повторить"
Здравствуйте, x-code, Вы писали:
XC>Вообще интересно... "Box" — это некая обертка вокруг значения. Это может быть дополнительная информация (option, either). Или несколько равноправных значений (list). Или вообще функция, которая это значение рассчитывает (асинхронное выполнение). Все это очень разные понятия. XC>Сама по себе концепция этой обертки уже очень интересна, без всяких монад. Есть ли какое-то общее определение этой "обертки" Что еще может быть оберткой?
В общем случае это любое отображение Тип -> Тип (для которого можно реализовать упомянутые выше операции). Тут как раз полезно вспомнить исходное понятие из математики. Всякая монада — это прежде всего функтор, а функтор состоит из двух отображений: первое отображает объекты категории в объекты категории (в нашем случае — типы в типы), а второе отображает стрелки в стрелки (т.е. в нашем случае — функции в функции). Про второе (map) maxkar написал, про первое в явном виде забыл. Т.е. Box — это функция, отображающая тип-аргумент Т в некоторый другой тип. Это может быть класс Box<T>, а может быть функция String -> T, а может быть что-то еще, любой тип, зависящий от Т и удовлетворяющий нужным нам свойствам.
XC>То есть конструктор будущей монады? Берем простое значение и заворачиваем его в бокс. А вот кстати, чтобы взять обратно значение из бокса есть что-нибудь?
У классической монады такой операции разворачивания нет, и это важно. Если б можно было так просто разворачивать, то не получилось бы использовать монады для описания эффектов. Можно было бы грязную функцию раз и развернуть, превратить в чистую, это бы все поломало.
XC>То есть как-бы получается, что для того, чтобы сделать что-то со значением, находящимся в боксе, нужно чтобы у этого бокса был специальный метод, принимающий на вход функцию-обработчик? И это, как я понимаю, замена возможности получения значения, хранящегося в боксе. Интересный подход.
Именно так. Мы можем работать с завернутыми в монаду значениями только оставаясь внутри монады (передавая туда функции), и результат остается завернутым.
XC>То есть метод map() или apply() сделали статической функцией, а бокс ей передаем в качестве второго аргумента? ОК. Пока непонятно зачем это.
Потому что на самом деле map функтора — это отображение функций:
map : (A -> B) -> (Box<A> -> Box<B>)
что эквивалентно
map : (A -> B) -> Box<A> -> Box<B>
что, через uncurrying эквивалентно
map : ((A -> B) , Box<A>) -> Box<B>
И чаще всего map так и используется — не значения мапить, а функции.
XC>На мой неопытный взгляд тут расхождение со статьей на Хабре, на оригинал которой вы ссылались. XC>Там написано что "аппликативный функтор" это когда функция тоже упакована в контейнер, а у вас нечто другое — когда функция применяется для группы "боксов" сразу. Или я чего-то не понял?
Это тоже эквивалентные вещи. Если мы возьмем чистую фунцию
f : (A , B) -> C
что эквивалентно
f : A -> B -> C
Иฺ завернем ее в аппликативный функтор через return
return f : Box< A -> B -> C >
То сможем применить аппликацию из аппликативного функтора
<$> : Box <A -> B> -> Box<A> -> Box<B>
(return f) <$> (return a) : Box <B -> C>
(return f) <$> (return a) <$> (return b) : Box <C>
автоматической проверки errno и прерывания исполнения функции, если что не так — стандартная сишная задача. Если бы были монады — можно было бы спрятать это внутрь оной и код стал бы на порядок чище.
E>Так а чем это в таком случае принципиально отличается от продвинутых макросов? Подозреваю в чем-то вроде немерла с использованием макросов можно сделать тоже самое.
Вот такое лобовое приминение — ничем. (В этом смысле и функции можно на макросах реализовать)
Но монады (как и функции), в отличие от макросов, являются полноценными первоклассными объектами.
Они участвуют в системе типов, позволяя четко разграничивать код, завязанный на монады, и чистый код.
Они сами являются типами, т.е. для них возможно все, что возможно для типов.
Они могут скрещиваться друг с другом, давая монады более высокого порядка (например, монада future (в терминах С++11, т.е. асинхронное вычисление) может скрещиваться с монадой Maybe на случай, если вычисление бросит исключение и получение результата будет в принципе невозможным).
Для них даже есть Monad Comprehension (а List Comprehension становится просто тривиальным частным случаем, так как список — это тоже монада).
Для них можно писать функции, работающий с монадами непосредственно (так же, как можно писать функции, работающий с функциями — т.е. функции высшего порядка).
А дело все в том, что в функциональныхз языках ничего, кроме функций, по сути, нет. Зато есть очень широкие возможности по преобразованию функций и вызовов функций.
Функциональная программа — это просто гигантский вызов функции, которая "как-то" вызывает другие функции и далее до самого конца.
И фишка в том, что, так как ничего, кроме функций, нет, и все собирается из разной степени вывернутости вызовов функциями друг друга, можно вот упомянутым "как-то" управлять и преобразовывать то, как функции друг друга зовут. Этим и занимаются монады и подобные им конструкты.
А в императивных языках у тебя, кроме вызовов функций, есть еще императивные инструкции (циклы, условия, объявления переменных, и т.п). И если в достаточно продвинутом императивном языке ты на его функциональной части еще можешь навернуть нечто, похожее на монады, то вот с инструкциями ты ничего сделать не сможешь. Если только они сами не реализованы в функциональном стиле в виде макросов типа Немерла. Но такие макросы все равно будут "над" языком, в отличие от монад в функциональныхз языках, где они "внутри" и с ними можно работать.
Здравствуйте, jazzer, Вы писали:
J>Но монады (как и функции), в отличие от макросов, являются полноценными первоклассными объектами. J>Они участвуют в системе типов, позволяя четко разграничивать код, завязанный на монады, и чистый код.
Разве код, завязанный на монады, грязный?
Здравствуйте, samius, Вы писали:
S>Здравствуйте, jazzer, Вы писали:
J>>Но монады (как и функции), в отличие от макросов, являются полноценными первоклассными объектами. J>>Они участвуют в системе типов, позволяя четко разграничивать код, завязанный на монады, и чистый код. S>Разве код, завязанный на монады, грязный?
Загрязненный монадой, я имею в виду
Здравствуйте, jazzer, Вы писали:
J>Здравствуйте, samius, Вы писали:
S>>Здравствуйте, jazzer, Вы писали:
J>>>Но монады (как и функции), в отличие от макросов, являются полноценными первоклассными объектами. J>>>Они участвуют в системе типов, позволяя четко разграничивать код, завязанный на монады, и чистый код. S>>Разве код, завязанный на монады, грязный? J>Загрязненный монадой, я имею в виду
и чистый от монады, соответственно? Тогда некоторым языкам повезло — они чисты как слеза младенца