Funciones Map, Reduce, Filter: iteración de alto nivel
Después de trabajar con Smalltalk, Lisp o Python, todos ellos lenguajes que poseen en mayor o menor grado estructuras de iteración avanzadas, se hace muy difícil utilizar mecanismos más primitivos, es el caso de las funciones en Cocoa.
Dado que Objective C posee bloques, condición sine qua non para implementar funciones (métodos en nuestro caso) similares a map, reduce, filter, daba por hecho que Cocoa traía “de serie” una implementación de dichos métodos. Cual no sería mi sorpresa al ver que no era sí.
Map
La primera de las funciones en cocoa que veremos es Map. Originalmente desarrollada en Lisp, la función map toma como parámetro otra función y una lista. Aplica dicha función sobre todos los elementos de la lista y devuelve una nueva lista con los resultados.
Es decir, si tenemos un lista con números: (1 2 3 4) y una función que suma 1 a su parámetro, si utilizamos map con dichos parámetros, el resultado sería la lista (2 3 4 5).
Por supuesto que esto mismo se podría hacer con un bucle for, sin embargo, con map evitamos repetir código y evitamos errores comunes como hacer una iteración de más o de menos.
Es decir, map está para los bucles como la OOP para la programación estándar.
En Smalltalk, “alma mater” de Objective C, dicha funcionalidad está proporcionada por el método collect: que acepta como parámetro un bloque.
Adaptando esto a la terminología de Cocoa, crearemos el siguiente método:
typedef id (^collectorBlock) (id element); -(NSArray *) arrayByCollectingWithBlock: (collectorBlock) block;
Ahora, si tenemos un NSArray con cadenas y queremos estandarizar el uso de mayúsculas y minúsculas, nuestra primera reacción no debería de ser escribir un bucle for, crear un NSArray para los resultados, etc. Sólo tenemos que preocuparnos con la transformación que queremos aplicar a los elementos:
NSArray *lowercase = [NSArray arrayWithObjects:@"one", @"two", @"three", @"four", nil]; NSArray *uppercase = [lowercase arrayByCollectingWithBlock:^id(id element) { return [element uppercaseString]; }];
Sólo tenemos que tener cuidado en no permitir que el bloque que nos pasen devuelva nil, ya que al intentar colocar un nil en el NSArray resultante recibiremos una excepción (no se permite nil en ninguna colección). Para eso utilizamos una función que en caso de recibir un nil, devuelve un NSNull:
static inline id nullifyIfNil(id elt){ if (!elt) { elt = [NSNull null]; } return elt; }
Filter
También originalmente de Lisp, filter permite obtener una nueva lista con aquellos elementos que pasan un test, representado por una función que acepta un elemento y devuelve un valor booleano.
En Smalltalk dicha función viene representada por el método select: que acepta un bloque. Nosotros crearemos el método:
typedef BOOL (^filterBlock)(id element); -(NSArray *) arrayByFilteringWithBlock: (filterBlock) block;
Si solo queremos las cadenas:
NSArray *test = [NSArray arrayWithObjects:@"One", @"two", [NSNumber numberWithInt:3], [NSNumber numberWithBool:NO], [NSNull null], [[NSDate alloc] init], nil]; NSArray *strings = [test arrayByFilteringWithBlock:^BOOL(id element) { return [element isKindOfClass:[NSString class]]; }];
Reduce
Otro clásico de Lisp y los lenguajes funcionales en general. Al contrario de las anteriores, no devuelve una lista, sino un escalar. Dicho valor se obtiene combinando de alguna forma los elementos de la lista original. El ejemplo más simplón sería sumar los elementos de una lista.
Se utiliza un bloque que recibe 2 parámetros y se invoca para cada elemento de la lista, pasándole:
- Un valor inicial y el primer elemento de la lista
- Para cada llamada posterior, se le pasa el resultado de la llamada anterior y el siguiente elemento de la lista.
En Smalltalk se utiliza el método inject:into:. Siempre me ha parecido un nombre estrafalario que no aclara muy bien lo que hace el método. Para Objective C, he optado por un nombre que espero resulte más claro:
typedef id (^reducerBlock) (id firstElement, id nextElement); -(id) valueByReducingWithBlock: (reducerBlock) block startingWith: (id) start;
Posibles mejoras
No considero ni mucho menos el código como terminado, ya que se podría extender a otras colecciones (de momento sólo es una categoría sobre NSArray), incluir otros métodos usados en Smalltalk como puede ser detect: (devuelve el primer elemento que pasa un test), etc.
De lejos lo más interesante sería hacer versiones multihebra, usando GCD. De esta manera se podría operar en paralelo sobre la colección. Esto posiblemente sea interesante para caso de colecciones muy grandes, y poniendo un límite al número de hebras utilizadas.