JS: Funciones

Teoría: Señales

El método sort() demuestra bien la importancia y conveniencia de las funciones de orden superior para resolver tareas cotidianas. Al describir el algoritmo una vez, podemos obtener diferentes variantes de comportamiento especificándolas directamente en el lugar de la clasificación. Lo mismo se aplica a los métodos map(), filter() y reduce() discutidos.

Al utilizar funciones de orden superior, es común dividir la tarea en sub tareas y ejecutarlas secuencialmente, enlazándolas en una cadena de operaciones. Esta solución se asemeja a arrastrar datos a través de una cadena de funciones transformadoras.

En SICP, este enfoque se compara con el procesamiento de señales en el diseño de circuitos eléctricos. La corriente, al pasar por el circuito, pasa a través de una cadena de transformadores: filtros, supresores de ruido, amplificadores, etc. En este caso, el voltaje (y la corriente que crea) desempeña el papel de los datos, y los transformadores desempeñan el papel de las funciones.

Esta forma de trabajar con colecciones en JavaScript es fundamental. Sin embargo, los bucles casi no se utilizan debido a su menor flexibilidad, mayor cantidad de código (más propenso a errores) y dificultades para dividir un algoritmo complejo en pasos independientes.

Supongamos que estamos escribiendo una función que toma una lista de rutas del sistema de archivos, encuentra los archivos con la extensión js sin tener en cuenta el caso y devuelve los nombres de esos archivos. Para resolver esta tarea, necesitaremos las siguientes funciones:

const getJSFileNames = (paths) => {
  const result = [];
  // Enfoque opuesto al procesamiento de flujo.
  // Aquí todo se ejecuta de una vez sin dividirlo en pasos.
  for (const filepath of paths) {
    // Extraemos la extensión
    const extension = path.extname(filepath).toLowerCase();
    // Si la ruta existe, es un archivo y tiene la extensión .js
    if (fs.existsSync(filepath) && fs.lstatSync(filepath).isFile() && extension === '.js') {
      // Normalizamos la ruta y la agregamos a la lista de resultados
      result.push(path.basename(filepath.toLowerCase(), extension));
    }
  }

  return result;
};

const names = getJSFileNames(['index.js', 'wop.JS', 'nonexists', 'node_modules']);
console.log(names); // => [index, wop]

En el ejemplo anterior, se muestra una solución típica utilizando bucles. Su algoritmo se puede describir de la siguiente manera:

  1. Recorremos cada ruta.
  2. Si la ruta actual es un archivo normal con la extensión .js (sin tener en cuenta el caso), la agregamos a la matriz de resultados.

Si intentamos hacer lo mismo utilizando el método reduce(), obtendremos un código idéntico a la solución con bucles. Pero si pensamos detenidamente, veremos que esta tarea se divide en dos: filtrar y mapear.

const getJsFileNames = (paths) => paths
   // filtramos los archivos que realmente existen
  .filter((filepath) => fs.existsSync(filepath))
   // filtramos por tipo de archivo
  .filter((filepath) => fs.lstatSync(filepath).isFile())
   // filtramos por extensión
  .filter((filepath) => path.extname(filepath).toLowerCase() === '.js')
   // mapeamos a nombres (necesitamos una matriz de nombres)
  .map((filepath) => path.basename(filepath.toLowerCase(), '.js'));

const names = getJsFileNames(['index.js', 'wop.JS', 'nonexists', 'node_modules']);
console.log(names); // => [index, wop]

El código resultante es un poco más corto (sin contar los comentarios) y más expresivo, pero lo más importante no es su tamaño. A medida que aumenta la cantidad de operaciones y su complejidad, el código dividido de esta manera es mucho más fácil de leer y analizar, ya que cada operación se realiza de forma independiente para todo el conjunto de datos. Es necesario mantener menos detalles en la mente y se puede ver de inmediato cómo afecta cada operación a los datos en su conjunto. Sin embargo, aprender a dividir correctamente una tarea en sub tareas no es tan fácil como puede parecer al principio. Se necesita algo de práctica y experiencia antes de que su código sea legible.

Ten en cuenta que aquí la filtración se divide en tres pasos en lugar de hacerse en uno solo. Dado lo conciso que es la definición de funciones en JavaScript, es mucho mejor dividir las comprobaciones en más filtros que hacer un solo filtro complejo.

Interfaces estándar

La posibilidad misma de esta división se basa en una idea simple que a veces se llama "interfaces estándar". Consiste en que se espera el mismo tipo de datos en la entrada y salida de las funciones, en nuestro caso, una matriz. Esto permite combinar funciones y construir cadenas que realizan una gran cantidad de tareas diferentes sin necesidad de implementar nuevas funciones. Las operaciones mencionadas anteriormente: mapeo, filtrado y agregación, combinadas entre sí, permiten resolver la gran mayoría de tareas de procesamiento de colecciones. Algo similar a esto lo hemos encontrado todos en nuestras vidas cuando construíamos conjuntos de Lego. Un pequeño número de piezas.

Completado

0 / 16