En este post invitado, Marcelo Alves inicia una serie sobre funciones de orden superior. ¡Gracias, Marcelo!

Como sabemos, las funciones son cajas negras que reciben un input, aplican una transformación y retornan un output. Pero, ¿qué pasa cuando el input es una colección y queremos aplicar una función a cada uno de los elementos?

Para ilustrar la situación nos plantearemos el siguiente problema: transformar una lista de círculos en una lista de cuadrados. Empezamos por hacerlo para solo un ítem.

Creamos el input:

//  ● = { radius: Number, color: String }
let circle = {  
  radius: 5,
  color: "red"
};

una función que lo transforma:

//  toSquare : ● -> ■
let toSquare = (circle) => {  
  return {
    side: circle.radius * 2,
    color: circle.color
  };
};

y aplicamos la transformación para obtener el output:

let square = toSquare(circle);  

Hasta aquí ningún problema. Pero, ¿qué pasa cuando tenemos un lista de círculos y la queremos transformar en una lista de cuadrados?

let circles = [  
  {
    radius: 5,
    color: "red"
  },
  {
    radius: 5,
    color: "green"
  },
  {
    radius: 5,
    color: "blue"
  }
];

Ahora nuestros círculos están dentro de un contenedor. Y nuestra función ya no sirve...

let squares = toSquare(circles); // { color: undefined, side: NaN }  

Necesitamos una función que pueda entrar en el contenedor, transformar cada valor aplicando la función que habíamos creado y volver a poner todo en un contenedor. La llamaremos toSquare; recibirá un array de círculos y devolverá un array de cuadrados.

//  toSquare : [●] -> [■]
let toSquare = (circles) => {  
  // creamos un array vacío para acumulará los cuadrados
  let squares = [];

  // iteramos el array de círculos
  for (let i = 0; i < circles.length; i++) {
    // obtenemos el círculo de la iteración actual
    let circle = circles[i];

    // transformamos el círculo en un cuadrado
    // con la función que habíamos creado
    let square = toSquare(circle);

    // y lo añadimos al array acumulador
    squares.push(square);
  }

  // por fin devolvemos todos los cuadrados
  return squares;
};

Ya lo tenemos.

let squares = toSquare(circles);  

Hay nuevos requerimientos. Nos piden transformar una lista de círculos en una lista de triángulos. Tenemos la función que lo hace para un solo triángulo,

//  toTriangle : ● -> ▲ 
let toTriangle = (circle) => {  
  let side = 3 * circle.radius / Math.sqrt(3);
  return {
    base: side,
    height: Math.sqrt((side * side) + ((side / 2) * (side / 2)))
  };
};

pero no una función que lo haga para una lista de triángulos.
Bueno, no pasa nada, si lo hemos podido hacer para cuadrados no será dificil hacerlo para triángulos.

//  toTriangle : [●] -> [▲]
let toTriangle = (circles) => {  
  let triangles = [];

  for (let i = 0; i < circles.length; i++) {
    let circle = circles[i];
    let triangle = toTriangle(circle);
    triangles.push(square);
  }

  return triangles;
}

Hmmm... Esta función es sospechosamente parecida a la que transformaba círculos en cuadrados. Solo cambia el tipo de forma geométrica. ¿Y si encapsulamos la función en una factory que devuelve formas geométricas distintas según el tipo que le pasamos?

//  toAnotherShape : ● -> String -> ▲ | ■ | ●
let toAnotherShape = (circle, shapeType) => {  
  if (shape === "circle") {
    return toTriangle(circle);
  } else if (shapeType === "square") {
    return toSquare(circle);
  } else {
    return circle;
  }
};

//  toAnotherShape : [●] -> String -> [▲] | [■] | [●]
let toAnotherShape = (circles, shapeType) => {  
  let otherShapes = [];

  for (let i = 0; i < circles.length; i++) {
    let circle = circles[i];
    let otherShape = toAnotherShape(circle, shapeType);
    otherShapes.push(square);
  }

  return otherShapes;
}

let squares = toAnotherShape(circles, "square");  
let triangles = toAnotherShape(circles, "triangle");

Vamos mejorando; hemos encapsulado la iteración del array en una sola función. Pero pasar un shapeType me parece un hack. Además toAnotherShape funciona para cambios de tipo pero, ¿y si tuviéramos una función que cambia el color y no la forma? Tendríamos que repetir el bucle y aplicar la función para cada círculo otra vez. Tiene que haber una abstracción mejor.

Hay nuevos requerimentos otra vez. Nos han pasado una función que pinta círculos de verde y quieren transformar una lista de círculos de varios colores en una lista de círculos verdes.

//  greenify : ● -> ●
let greenify = (circle) => {  
  return Object.assign({}, circle, {
    color: "green"
  });
};

La función toAnotherShape ya no nos vale. No sabemos qué transformación le vamos a aplicar, pero nos gustaría mantener la lógica de iteración. Si inicialmente no sabemos qué transformación vamos a aplicar a cada elemento, hay que pasar la función dentro. Llamaremos a esa función transform y toAnotherShape pasará a llamarse transformEach.

let transformEach = (transform, things) => {  
  let otherThings = [];

  for (let i = 0; i < things.length; i++) {
    let thing = things[i];
    let anotherThing = transform(thing);
    otherThings.push(anotherThing);
  }

  return otherThings;
}

let squares = transformEach(toSquare, circles);  
let greenCircles = transformEach(greenify, circles);  

La función que acabamos de crear ya existe, pero con otro nombre.
Se llama map y está implementada como un método del objeto Array de Javascript desde la versión ES5.

Entonces, volvamos al problema inicial: transformar una lista de círculos en una lista de cuadrados. Con la función map lo hacemos en una línea de código, declarando lo que queremos hacer. No sabemos cómo lo está haciendo por dentro, pero nos vale para cualquier array y cualquier función que opere sobre elementos de ese array.

let squares = circles.map(toSquare);  

En ningún momento en la expresión de arriba se habla de una variable i que empiece en cero y que va hasta la longitud del array, ni de un array auxiliar que se vaya rellenando. map es una función que recibe otra función y, como tal, no le importa qué transformación vamos a aplicar, ni qué tipo devolvemos.

map es el nuevo for si queremos trabajar en un nivel de abstracción más alto.

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Marcelo Alves es Frontend Engineer en SchibstedSpain. Podéis encontrarlo en @marcelofpalves.

Suscríbete a mi lista de correo

* Campos obligatorios