Se você se deparar com funções que chamam outras funções em sequência, perceberá rapidamente que é um pesadelo de manutenção e a reutilização de código é praticamente inexistente.
tl; dr
Se você deseja apenas ver uma Mônada de Identidade de dez linhas para entrelaçar funções, role para baixo até o final.
Um exemplo:
readFile('dummy.json');/*{ "text":"...", "url":"http://www.blame.it" }*/
function readFile(filename) {
getAjax(/* read the file */);
}
function getAjax(json) {
var xhr = new XMLHttpRequest();
xhr.open('GET', json.url);
get.onreadystatechange = function() {
/* check this.readyState */
if(this.responseText) {
changeUI(this.responseText);
}
}
xhr.send();
}
function changeUI(data) {
var dataObject;
if(data) {
try {
dataObject = JSON.parse(data);
} catch (e) { return; }
/* change the UI (usually the DOM) with the new content */
}
}
Olhando para readFile, você será perdoado por pensar que seu propósito é ler um arquivo. Na verdade, sua responsabilidade é ler um arquivo E iniciar uma cadeia de funções que, posteriormente, terminará com uma alteração da IU. ReadFile ou getAjax não são reutilizáveis. Levando à duplicação de código.
Callbacks
Agora, isso pode ser um pouco atenuado usando callbacks.
readFile(function(json) {
getAjax(function(data) {
changeUI(data);
}, json);
}, 'dummy.json');
function readFile(callback, filename) {
callback(/* read a file and get an URL */);
}
function getAjax(callback, json) {
var xhr = new XMLHttpRequest();
xhr.open('GET', json.url);
get.onreadystatechange = function() {
/* check this.readyState */
if(this.responseText) {
callback(this.responseText);
}
}
xhr.send();
}
function changeUI(callback, data) {
var dataObject;
if(data) {
try {
dataObject = JSON.parse(data);
} catch (e) { return; }
/* change the UI (usually the DOM) with the new content */
callback();
}
}
Agora podemos reutilizar todas as funções e apenas fornecer um retorno de chamada de nossa escolha para encadear a sequência de funções.
readFile(function(json) { //<- read local file
changeUI(function() { //<- change UI with local text
getAjax(function(data) { //<- get data from blame.it
changeUI(function() {}, data); //<- change UI with remote text
}, 'http://www.blame.it');
}, json);
}, 'dummy.json');
Mas temos que usar a sintaxe complicada, conhecida como callback hell. Os desenvolvedores de Node.js devem estar familiarizados com o conceito.
Mônada
O que gostaríamos de fazer é criar uma composição de funções que podemos usar e reutilizar. Acontece que isso é fácil de resolver com Mônadas. Mônadas é um padrão que separa a lógica do fluxo de trabalho da lógica de operação, ei suas funções. Isso permite que você crie composições de funções que você pode chamar. Criei dez linhas de código, função monádica, que chamo de otimista .
O objeto de promessa do jQuery está em vigor, um Identity Monad de forma assíncrona e, portanto, otimista .
É assim que é usado:
optimist(readFile, getAjax, changeUI)('dummy.json');
Ele pega suas funções como argumentos e você as chama com um valor de semente , que será o primeiro argumento para a primeira função.
Como o otimista cria uma composição de funções, também podemos distribuí-las e chamá-las posteriormente.
var online = optimist(readFile, getAjax, changeUI);
var offline = optimist(readFile, changeUI);
if(navigator.onLine/*or Ti.network.online if you're using Titanium*/) {
online('online.json');
} else {
offline('offline.json');
}
// OR
var updateUI = {
online: optimist(readFile, getAjax, changeUI),
offline: optimist(readFile, changeUI)
}
someFunctionThatChoosesASeedFileName(updateUI);
Agora, eu chamo isso de otimista Monad porque ele assume que você deseja executar todas as funções fornecidas, a menos que haja uma rejeição, então ele para. A rejeição está implícita, como se você não ligasse para o retorno, é uma rejeição. Ele também assume que os argumentos retornados de uma função são úteis para a próxima função e não lembra os argumentos passados de funções anteriores à atual. Chega de conversa, aqui está:
var optimist = function() {
function next(){
var args = getArguments.call(arguments);
var currentFn = this.shift();
if(!currentFn) return; // done
currentFn.apply(this, [promise].concat(args));
}
function getArguments() {
return Array.prototype.splice.call(this, 0, this.length);
}
var args = getArguments.call(arguments);// you can put this
var promise = next.bind(args); // inside the bind call
return promise;
}
O otimista transforma todos os seus retornos de chamada em promessas, que você pode decidir resolver quando sua operação estiver concluída. Você não retorna um valor com a return
palavra – chave, mas chama a promessa com seu valor de retorno. Não há bifurcação da cadeia, apenas uma sequência linear de cálculos no futuro.
Vamos decompô-lo.
var optimist = function() {
...
/* turn the supplied functions into an Array */
var args = getArguments.call(arguments);
/* bind the Array of functions to the next function,
which turns the next function into an Array */
var promise = next.bind(args);
return promise;
}
Quando você chama otimista pela primeira vez , definimos promessa como a próxima função com this
(o contexto / escopo) definido como um Arary das funções fornecidas e retornamos isso ao consumidor de otimista .
var optimist = function() {
function next(){
/* get the arguments from the current invoked function
in the first run, that would be the seed value(s) */
var args = getArguments.call(arguments);
/* since the next function is now an Array
we can use shift to get the first function
meaning that next is implemented as a FIFO queue */
var currentFn = this.shift();
/* if there is no more functions in the queue we are done */
if(!currentFn) return; // done
/* call the current function with the arguments from the last
function and prepend the promise */
currentFn.apply(this, [promise].concat(args));
}
...
}
Quando você chama o resultado de otimista , com ou sem um valor semente, a cadeia de invocações de função começa. A próxima função é uma fila Primeiro a Entrar, Primeiro a Sair e é onde toda a mágica acontece.
Obtemos os argumentos da invocação da função atual e os atribuímos a args .
Em seguida, retiramos da fila uma função (lembre-se de que next é um Array de funções) e chamamos essa função com o objeto de promessa e args , em que a promessa é uma referência especial para next , que é vinculado às funções fornecidas. Não há nenhuma razão real para definir o contexto this
, aqui. Você pode configurá-lo para currentFnou args . Definir como this
significa que as funções têm acesso a funções futuras. Se é útil ou não, não tenho certeza.
A função currentFn chamará a promessa como seu retorno de chamada e, portanto, iniciará a próxima iteração.
As funções tecidas podem ser classes de Programação Orientada a Objetos (OOP). Deixe-me ilustrar:
function Person(name) {
this.name = name;
}
Person.prototype.says = function(callback, to) {
callback(this.name + " says hi to " + to);
}
var hansel = new Person("Hänsel");
optimist(
hansel.says.bind(hansel),
function(greet) {
console.log(greet);
}
)("Gretel");
O código
Aqui está o código completo:
var optimist = function() {
function next(){
var args = getArguments.call(arguments);
var currentFn = this.shift();
if(!currentFn) return; // done
currentFn.apply(this, [promise].concat(args));
}
function getArguments() {
return Array.prototype.splice.call(this, 0, this.length);
}
var promise = next.bind(getArguments.call(arguments));
return promise;
}
// run all functions immediately
optimist(readFile, getAjax, changeUI)('dummy.json');
// create compositions for later use
var online = optimist(readFile, getAjax, changeUI);
var offline = optimist(readFile, changeUI);
if(navigator.onLine/*or Ti.network.online if you're using Titanium*/) {
online('online.json');
} else {
offline('offline.json');
}
// OR
var updateUI = {
online: optimist(readFile, getAjax, changeUI),
offline: optimist(readFile, changeUI)
}
someFunctionThatChoosesASeedFileName(updateUI);
// run functions and class methods in sequence
function Person(name) {
this.name = name;
}
Person.prototype.says = function(callback, to) {
callback(this.name + " says hi to " + to);
}
var hansel = new Person("Hänsel");
optimist(
hansel.says.bind(hansel),
function(greet) {
console.log(greet);
}
)("Gretel");
function readFile(callback, filename) {
callback(/* read a file and get an URL */);
}
function getAjax(callback, json) {
var xhr = new XMLHttpRequest();
xhr.open('GET', json.url);
get.onreadystatechange = function() {
/* check this.readyState */
if(this.responseText) {
callback(this.responseText);
}
}
xhr.send();
}
function changeUI(callback, data) {
var dataObject;
if(data) {
try {
dataObject = JSON.parse(data);
} catch (e) { return; }
/* change the UI (usually the DOM) with the new content */
callback();
}
}
Até agora otimista , tem apenas um dia e só foi usado na minha proposta de espaço em branco CSS para W3C . Você pode ver o impacto entre white-space.modern.js e white-space.js , onde o primeiro usa o otimista .
Boa codificação!
Leitura adicional: