IO как первый в цепочке композиций
Мне интересно экспериментировать с монадами ввода-вывода, подобными Haskell, в моих композициях функций JavaScript.
Что-то вроде Folktale has Task похоже на IO в Haskell тем, что он ленив и, следовательно, технически чист. Он представляет собой действие, которое может произойти в будущем. Но у меня есть несколько вопросов.
Как сформировать композицию функций, если все последние функции зависят от возвращаемого значения исходной нечистой функции в композиции? Сначала нужно запустить фактическую задачу, неявно передавая возвращенные данные функциям, находящимся ниже по строке. Нельзя просто передать неразрешенную Задачу, чтобы сделать что-нибудь полезное, или можно? Это выглядело бы примерно так.
compose(doSomethingWithData, getDataFromServer.run());
Я, наверное, упускаю что-то важное, но в чем польза?
Связанный с этим вопрос: какое конкретное преимущество имеет ленивое вычисление нечистой функции? Конечно, он обеспечивает ссылочную прозрачность, но суть понимания проблемы - это структура данных, возвращаемая нечистой функцией. Все последние функции, передающие данные по конвейеру, зависят от данных. Так какую же пользу нам дает ссылочная прозрачность нечистых функций?
РЕДАКТИРОВАТЬ: Итак, посмотрев некоторые ответы, я смог легко составлять задачи, объединяя их в цепочку, но я предпочитаю эргономику использования функции составления. Это работает, но мне интересно, идиоматично ли это для функциональных программистов:
const getNames = () =>
task(res =>
setTimeout(() => {
return res.resolve([{ last: "cohen" }, { last: "kustanowitz" }]);
}, 1500)
);
const addName = tsk => {
return tsk.chain(names =>
task(resolver => {
const nms = [...names];
nms.push({ last: "bar" });
resolver.resolve(nms);
})
);
};
const f = compose(
addName,
getNames
);
const data = await f()
.run()
.promise();
// [ { last: 'cohen' }, { last: 'kustanowitz' }, { last: 'bar' } ]
Затем возникает еще один вопрос, возможно, более связанный со стилем: теперь мы должны составить функции, которые все работают с задачами, что кажется менее элегантным и менее общим, чем те, которые имеют дело с массивами / объектами.
2 ответа
Как сформировать композицию функций, если все последние функции зависят от возвращаемого значения исходной нечистой функции в композиции?
В chain
используется для создания монад. Рассмотрим следующие голые костиTask
пример.
// Task :: ((a -> Unit) -> Unit) -> Task a
const Task = runTask => ({
constructor: Task, runTask,
chain: next => Task(callback => runTask(value => next(value).runTask(callback)))
});
// sleep :: Int -> Task Int
const sleep = ms => Task(callback => {
setTimeout(start => {
callback(Date.now() - start);
}, ms, Date.now());
});
// main :: Task Int
const main = sleep(5000).chain(delay => {
console.log("%d seconds later....", delay / 1000);
return sleep(5000);
});
// main is only executed when we call runTask
main.runTask(delay => {
console.log("%d more seconds later....", delay / 1000);
});
Сначала нужно запустить фактическую задачу, неявно передавая возвращенные данные функциям, находящимся ниже по строке.
Верный. Однако выполнение задачи можно отложить.
Нельзя просто передать неразрешенную Задачу, чтобы сделать что-нибудь полезное, или можно?
Как я продемонстрировал выше, вы действительно можете составлять задачи, которые еще не начались, используя chain
метод.
Связанный с этим вопрос: какое конкретное преимущество имеет ленивое вычисление нечистой функции?
Это действительно обширный вопрос. Возможно, вас может заинтересовать следующий вопрос SO.
Так какую же пользу нам дает ссылочная прозрачность нечистых функций?
Цитата из Википедии [ 1 ].
Важность ссылочной прозрачности заключается в том, что она позволяет программисту и компилятору рассуждать о поведении программы как о системе перезаписи. Это может помочь в проверке правильности, упрощении алгоритма, помощи в изменении кода без его нарушения или оптимизации кода с помощью мемоизации, исключения общих подвыражений, ленивого вычисления или распараллеливания.
Как мы можем выразить тип ввода-вывода Haskell в Javascript? На самом деле мы не можем, потому что в Haskell IO - это особый тип, глубоко связанный со средой выполнения. Единственное свойство, которое мы можем имитировать в Javascript, - это ленивое вычисление с явными преобразователями:
const Defer = thunk => ({
get runDefer() {return thunk()}
}));
Обычно ленивая оценка сопровождается обменом информацией, но для удобства я опускаю эту деталь.
Как бы вы составили такой тип? Что ж, вам нужно составить это в контексте thunks. Единственный способ лениво составлять thunks - это вложить их, а не сразу вызывать. В результате вы не можете использовать композицию функций, которая просто предоставляет функциональный экземпляр функций. Вам понадобится аппликативный (ap
/of
) и монада (chain
) экземпляры Defer
связать или, скорее, вложить их в гнездо.
Фундаментальной чертой аппликативов и монад является то, что вы не можете избежать их контекста, то есть, когда результат ваших вычислений находится внутри аппликативов / монад, вы не можете просто развернуть их снова. * Все последующие вычисления должны выполняться в соответствующих контекст. Как я уже упоминал сDefer
контекст - thunks.
Итак, в конечном итоге, когда вы составляете thunks с ap
/chain
вы создаете вложенное дерево вызовов отложенных функций, которое оценивается только при вызове runDefer
внешнего преобразователя.
Это означает, что ваша композиция преобразователя или цепочка остаются чистыми до первого runDefer
призыв. Это довольно полезное свойство, к которому мы все должны стремиться.
* вы, конечно, можете избежать монады в Javascript, но это уже не монада, и вы теряете все предсказуемое поведение.