Iterators, más que simple iteraciones

En la entrada anterior (Patrones de Diseño y JavaScript: Iterator) vimos como implementar una iteración simple (each) en objects que cumplan con una interfaz requerida (método _next). En esta entrada vamos a analizar las utilidades que son posibles gracias al uso de funciones como iteradores, donde comportamiento mediante se pueden reducir tareas complejas a simples iteraciones.

Para estos ejemplos vamos a utilizar el objeto Iterable, del cual vamos a extender nuestra clase Range (que NO ES una colección), y como interfaz requerida tenemos el método _next y opcionalmente el método _reset, que devuelve el objeto a su estado inicial.

En esta entrada extendemos el constructor Iterable, agregandole los métodos reduce, size, map, all, any y detect.

Nuestra implementación inicial de nuestro object Iterable (que nos provee únicamente del método each):

/**
 * Constructor que provee de la funcionalidad
 * de iteracion
 *
 * @constructor
 */
function Iterable() {}

/**
 * Excepcion utilizada para detener la iteracion
 */
Iterable.StopException = function() {};

/**
 * Itera los elementos provistos por el sujeto
 *
 * @param {Function} iterator
 * @param {Object} context
 * @return {Object} el sujeto
 */
Iterable.prototype.each = function(iterator, context) {
    if(typeof this._next != 'function')
        throw new Error('Unimplemented method: _next');

    try {
        iterator.call(context, this._next());
        return this.each(iterator, context);
    } catch(e) {
        if(e instanceof Iterable.StopException)
            return this
        throw e;
    }

    if(typeof this._reset == 'function')
		this._reset();

    return this;
};

La implementación del constructor Range sería la siguiente:

/**
 * Genera un rango numerico iterable
 *
 * @param {Number} from
 * @param {Number} to
 * @constructor
 */
function Range (from, to) {
    this.actual = this.from = from;
    this.to = to;
}

// Extendemos el constructor iterador
Range.prototype = new Iterable();

/**
 * Implementacion del metodo requerido por Iterator
 *
 * @return {Number}
 */
Range.prototype._next = function() {
    // Limitamos la ejecucion a los numeros que
    // pertenezcan al rango
    if(this.actual > this.to)
        throw new Iterable.StopException;

    // Devolvemos el indice actual
    return this.actual++;
};

/**
 * Implementacion del metodo
 */
Range.prototype._reset = function() {
	this.actual = this.from;
};

Teniendo esta implementación básica, podemos realizar una iteración sobre nuestro rango:

(new Range(0, 10)).each(console.log);

Luego, sobre el método each se pueden desarrollar distintas utilidades:

reduce

Reduce la colección generada a un único resultado, útil para generar un combinado de todos los elementos, ejemplo de esto, es obtener la sumatoria de los números que componen nuestro rango:

// Obtenemos la sumatoria del rango [0, 10]
(new Range(0, 10)).reduce(function(memo, e) {
	return memo + e;
}, 0); // 55

Implementación:

/**
 * Reduce el input multiple a un unico valor
 * a partir del valor inicial
 *
 * @param {Function} iterator
 * @param {Object} memo El valor incial
 * @param {Object} context
 * @return {Object}
 */
Iterable.prototype.reduce = function(iterator, memo, context) {
	this.each(function(e) {
		memo = iterator.call(context, memo, e);
	});

	return memo;
};

size

Devuelve el tamaño de la colección .

(new Range(0, 10)).size(); // 11

Sobre la implementación del reduce es muy sencillo agregar el size, teniendo cero como valor inicial, basta sumar uno por cada elemento:

/**
 * Devuelve el length calculado
 *
 * @return {Number}
 */
Iterable.prototype.size = function() {
	return this.reduce(function(memo, e) {
		return memo + 1;
	}, 0);
};

map

Devuelve una lista con el resultado de cada una de las transformaciones aplicadas:

// Obtenemos el cuadrado de cada numero
(new Range(0, 10)).map(function(e) {
	return e * e;
}); // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

La implementación se basa en iterar mediante el each e ir almacenando los resultados de cada iteración:

/**
 * Devuelve un array con la transformacion de
 * cada elemento
 *
 * @param {Function} iterator
 * @param {Object} context
 * @return {Array}
 */
Iterable.prototype.map = function(iterator, context) {
	var res = [];
	this.each(function(e) {
		res.push(
			iterator.call(context, e)
		);
	});
	return res;
};

all

Verifica si todos los elementos cumplen con la condición dada:

// Verificamos si todos los elementos son pares
(new Range(0, 10)).all(function(e) {
	return e % 2 == 0;
}); // false

Implementación:

/**
 * Verifica si todos los elementos pasan como ciertos
 * para el iterador
 *
 * @param {Function} iterator
 * @param {Object} context
 * @return {Boolean}
 */
Iterable.prototype.all = function(iterator, context) {
	var status = true;
	this.each(function(e) {
		// Si hay un falso, detenemos la iteracion
		if(!iterator.call(context, e)) {
			status = false;
			throw new Iterable.StopException;
		}
	});
	return status;
};

any

Verifica si al menos UNO de los elementos cumple con la condición dada:

// Verificamos si al menos un elemento par
(new Range(0, 10)).any(function(e) {
	return e % 2 == 0;
});// true

Implementación:

/**
 * Verifica si al menos un elemento cumple con la condicion
 *
 * @param {Function} iterator
 * @param {Object} context
 * @return {Boolean}
 */
Iterable.prototype.any = function(iterator, context) {
	var status = false;
	this.each(function(e) {
		// Si hay un true, detenemos la iteracion
		if(!iterator.call(context, e)) {
			status = true;
			throw new Iterable.StopException;
		}
	});
	return status;
};

detect

Devuelve el primer elemento que cumpla la condición dada:

// Obtenemos el primer numero mayor a cero divisible por 3 y por 2
(new Range(0, 10)).detect(function(e) {
	return e > 0 && e % 2 == 0 && e % 3 == 0;
}); // 6

Implementación:

/**
 * Deuvelve el primer elemento que cumpla la condicion
 *
 * @param {Function} iterator
 * @param {Object} context
 * @return {Boolean}
 */
Iterable.prototype.detect = function(iterator, context) {
	var el = null;
	this.each(function(e) {
		// Si hay un true, detenemos la iteracion
		if(iterator.call(context, e)) {
			el = e;
			throw new Iterable.StopException;
		}
	});
	return el;
};

 


Este código fuente puede ser encontrado en este gist.

Implementaciones similares a la de esta entrada se pueden encontrar en distintas librerías, siendo la precursora Prototype.js y la recomendada (por mí, por supuesto) hoy en día Underscore.js.

Además de los métodos aquí expuestos existen otros o similares con distinta nomenclatura, como ser filter, sortBy, pluck, reject, include, etc.

Leer más:

 


Shortlink: http://goo.gl/FNQQv




Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>