Re[3]: Что такое монады?
От: Klapaucius  
Дата: 23.11.10 11:32
Оценка: 53 (8)
Здравствуйте, <Аноним>, Вы писали:

А>Пока я из Вашего объяснения вижу некую абстракцию типа хитрого контейнера для хранения данных, причем непонятно в каких случаях ее целесообразно применять, и чем он лучше обычных массивов/списков и т.д. Ясно что я чего-то недопонимаю, и когда пойму мне самому смешно будет это читать Но что именно?


Это не абстракция "типа хитрого контейнера" это абстракция от контейнера, как, например, итератор — абстракция от контейнера. И массив и список это монады.

А>Спасибо, это все формально понятно. Но что это реально дает при программировании? Самое главное — зачем все это, как эта концепция (при должной поддержке со стороны языка — считаем что мы авторы языка и можем делать что хотим) реально облегчит жизнь программистам?


Двумя способами.
Первый способ
заключается в том, что мы разделяем логику и какую-то специфическую "обвязку" этой логики.
Представьте, что у нас есть функция f, которая что-то делает с двумя значениями, объектами. Эти два объекта мы можем получать разными способами. Допустим, что мы ищем их в некоем хранилище, базе данных, коллекции, т.е. они результат поиска, запроса. Можем найти, а можем и не найти.
X x = FindX();
Y y = FindY();
if(x.NotNull() && y.NotNull()) // если нужные нам значения получены
    return f(x, y); // вычисляем результат
else
  return null; // результат получить не можем, возвращаем ничего

Тут логика перепутана с проверкой возвращаемых значений, да и проверки реализуются для каждого использования поиска, по месту.
Разделяется и повторно используется она монадой Maybe
do {
    x <- FindX();
    y <- FindY();
    return f (x, y);
}

все, теперь проверки где-то за кадром, пишутся они один раз и потом работают.
Допустим, мы хотим получить результаты f для некоего набора значений. Проверить, например, функцию f на ее области определения или для каких-то особых случаев.
List<Z> result = new List<Z>();
for(X x in xs) {
    for(Y y in ys) {
        result.Add(f(x, y));
    }
}

Проход по наборам иксов и игреков также можно отделить от логики.
List<Z> result = do {
    x <- xs;
    y <- ys;
    return f(x, y);
}

Похоже на декартово произведение, записанное в нотации для множеств, что собственно нам и надо.
Разумеется, одна реализация интерфейса-монады для списка позволит писать и так
do {
    a <- alist;
    b <- blist;
    c <- clist;
    return foo(a,b,c);
}

т.е. за кулисами нашей монады могут быть циклы любой пложенности, каждый раз реализовывать работу с несколькими списками по месту нам больше не понадобится.
Точно также все будет работать и для других монад:
Парсим строку, получаем значения x и y, возращем результат применения f. Если x и y не распарсились — возвращаем ошибку парсинга.
do {
    x <- ParseX();
    y <- ParseY();
    return f (x, y);
}

Получаем значения по сети, ждем, пока придут оба, потом возвращаем результат применения f.
do {
    x <- Get("www.x.org");
    y <- Get("www.y.net");
    return f (x, y);
}

И так далее.
Более того, для монады можно реализовать функции, "поднимающие" другие функции в монады.
Parser parser = ParserMonad.Lift(f);

Все, теперь мы превратили функцию f в парсер. Даже do { <- <- ... <- return } писать не обязательно.
Необязательные свдения — для этих примеров достаточно аппликативного функтора — возможности монад несколько шире.
Второй способ
заключается в том, что монады (и функторы) обладают некоторыми свойствами. Что это значит? А вот что.
Все мы изучали в школе свойства сложения, умножения, тригонометрических функций и т.д. Они позволяли упростить некоторое громоздкое выражение и решить задачу.
Программирование — это решение задач. Упрощение там может пригодится, а может пригодится усложнение — оптимизация, но упрощение и усложнение, всякое преобразование кода в программировании это путь по минному полю. Мы можем думать, что отрефактореный код делает то же самое, но это не обязательно так. Как не покрывай код тестами, но покрытие это статистическое и баг может проскочить. Ну и проскакивает. В школе было проще, выражения равны, это доказано "багу" взяться неоткуда.
a*c + b*c == c*(a + b)

Это оптимизация. Мы заменили одну простую операцию и две сложных на одну простую и одну сложную.
Функторы, монады, моноиды и т.д. приводят школьную простоту и железную надежность доказательства в программирование. Если мы гарантируем выполнение простых свойств, то и сложные преобразования больших объемов кода с соблюдением этих простых правил будут корректны.
map f . map g == map f . g

Это свойство функтора (а следовательно и монады) и тоже оптимизация. Мы заменили работу со списком в два прохода эквивалентной работой в один проход.
До этого я писал, в основном, о монадах и функторах, а вопрос был еще и про стрелки. Тут у нас есть стрелки-функции (да, функция это стрелка) и комбинация этих стрелок:
f . g == x |-> g(f(x)) == fun (x) { return g(f(x)); }
//комбинация функций дает нам новую функцию.
id . f == f
//id - нейтральный элемент для ., как 0 нейтральный элемент для +, а 1 для *:
0 + x == x
1 * x == x

Если у нас есть
0 * ОченьСложноеВычисление

то нет смысла делать сложное вычисление — мы уже знаем ответ. Для map тоже есть значение, "отношения" операции map с которым похожи на отношение операции умножения с нулем. это id — функция, которая дает результат равный принимаемому значению.
map id огромныйСписок == id огромныйСписок == огромныйСписок

нет смысла проходить по огромному списку — он не изменится. "Какой-то бред", может подумать читатель "откуда у нас вдруг возьмется функция, которая ничего не делает?". Легко возмется. Есть множество пар действий, которые будучи сгруппированы (мы ведь упрощаем программу-выражение) дадут ничего не делающую функцию. Папример, поворот изображения по часовой стрелке в комбинации с поворотом изображения против часовой стрелки не делает ничего. есть также операции, которые становятся ничего не делающими при повторном применении, в комбинации с самими собой — зеркальное отражение изображения, например.
Другие свойства монад:
map f . unit == unit . f

Для того, чтобы собирать из монад конструкции из первой части, нам нужны специальные операции, которые монады соединяют.
Самая простая из них — рыбка (>=>).
Рыбка, как и . комбинирует стрелки, но не всякие, а стрелки Клейсли, т.е. функции, которые упаковывают значение (предмет) в монаду (коробку). Одну стрелку клейсли мы уже упоминали — это unit. Unit нейтральный элемент для рыбки, как id для . и 0 для +:
unit >=> f == f
f >=> unit == f

Рыбка ассоциативна, как и + и *:
(a >=> b) >=> c == a >=> (b >=> c)
(x + y) + z == x + (y + z)

Самая известная из них — bind (>>=), это когда мы сначала делаем map стрелкой Клейсли, а потом уменьшаем вложенность монад операцией join
m >>= f = join (map (f, m))

unit(a) >>= f == f(a)

кладем a в коробку
[a]

применяем к внутренностям коробки функцию f
[f(a)]

f это стрелка Клейсли, она кладет свой результат в коробку
[[b]]

а теперь джойним коробки
[b]

готово, то же что и просто
f(a) -> [b]

m >>= unit == m

тут все понятно, упаковываем содержимое коробки в коробку, добавляя вложенность коробок, а потом убираем лишнюю вложенность коробок — бессмысленное занятие.
m >>= (x |-> k x >>= h) = (m >>= k) >>= h

Это уже совсем не так весело, вот потому-то и начинать объяснение монад с bind — это ошибка. Смотрите лучше на join и рыбок.
map f m = m >>= unit . f

выражаем map через bind — упаковывая результат функции f в коробку.

Используя такой вот "алгебраический подход", можно рефакторить программы с обоснованной корректностью преобразования при условии выполнении свойств, а также писать, например, наивную реализацию, которую легко придумать и понять, а потом так же корректно трансформировать в реализацию эффективную, но, возможно, неочевидную и плохо читаемую. Пример таких преобразований на небольшой программе продемонстрирован в этой презентации, но материал не простой, не для первого чтения, потому как слайды в презентации перевернуты, ну а что поделать? В equational reasoning царской дороги нет. Также требуется знание синтаскиса и семантики языка haskell, что, конечно, не так сложно, как переворачивание слайдов, но, тем не менее, не совсем легко.

Резюмируя. Фактически, это аналог того процесса, который произошел гораздо раньше в физике. Два тысячелетия философы спорили о том, летит стрела Зенона или не летит, но вот составили набор простых правил и вуаля: производные находит ученик 10-го класса. Т.е. молодое ремесло "программирование" выпускается из детского сада и идет в школу с перспективой поступить в институт, получить диплом и стать обычной инженерной деятельностью. Вроде самолетостроения. Надеюсь, что на этот раз тысячелетий не понадобится.
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.