Здравствуйте, maxkar, Вы писали:
M>Для введения дальнейших понятий нам нужен еще какой-нибудь контейнер, в который можно "положить" значение любого типа. Пусть это будет Box<T>. Как у него семантика — зависит от реализации. Это может быть Option, может быть Either, может быть асинхронное выполнение, observable, collection, etc... Важно только то, что это некая "обертка" поверх типа.
Вообще интересно... "Box" — это некая обертка вокруг значения. Это может быть дополнительная информация (option, either). Или несколько равноправных значений (list). Или вообще функция, которая это значение рассчитывает (асинхронное выполнение). Все это очень разные понятия.
Сама по себе концепция этой обертки уже очень интересна, без всяких монад. Есть ли какое-то общее определение этой "обертки" Что еще может быть оберткой?
M>Здесь же можно отметить, что у Box должен быть метод "упаковки" простого значения в контейнер. Например, так:
M>M>Box.box : T => Box<T>
M>//применение
M>val boxed : Box<Int> = Box.box(3)
M>
M>Это является аналогом монадного return.
То есть конструктор будущей монады? Берем простое значение и заворачиваем его в бокс. А вот кстати, чтобы взять обратно значение из бокса есть что-нибудь?
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>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>
На мой неопытный взгляд тут расхождение
со статьей на Хабре, на оригинал которой вы ссылались.
Там написано что "аппликативный функтор" это когда функция тоже упакована в контейнер, а у вас нечто другое — когда функция применяется для группы "боксов" сразу. Или я чего-то не понял?
В общем если не сложно прокомментируйте мои комментарии, хотя-бы да/нет
В любом случае спасибо, объяснение супер, завтра продолжу разбираться с остальной частью.