Skip to content

Capítulo 06: Aplicación Práctica

Codificación Declarativa

Vamos a cambiar nuestra mentalidad. De ahora en adelante, dejaremos de decirle a la computadora cómo hacer su trabajo y en su lugar escribiremos una especificación del resultado deseado. Estoy seguro de que les resultará mucho menos estresante que intentar microgestionar todo constantemente.

Declarativo, en contraste con imperativo, significa que escribiremos expresiones en lugar de instrucciones paso a paso.

Piensen en SQL. No existe un "primero haz esto, luego aquello". Existe una expresión que especifica lo que deseamos de la base de datos. No decidimos cómo hacer el trabajo, lo hace el sistema. Cuando la base de datos se actualiza y el motor SQL se optimiza, no necesitamos modificar nuestra consulta. Esto ocurre porque existen múltiples formas de interpretar nuestra especificación y lograr el mismo resultado.

Para algunas personas, incluyéndome a mí, al principio es difícil comprender el concepto de codificación declarativa. Analicemos algunos ejemplos para familiarizarnos.

js
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
  makes.push(cars[i].make);
}

// declarative
const makes = cars.map(car => car.make);

El bucle imperativo primero debe instanciar el arreglo. El intérprete debe evaluar esta sentencia antes de continuar. Luego itera explícitamente sobre la lista de automóviles, incrementando manualmente un contador y mostrando sus partes en una exhibición explícita de iteración.

La versión con map es una sola expresión. No requiere orden de evaluación. Existe gran libertad en cómo itera la función map y cómo se ensambla el arreglo resultante. Especifica el qué, no el cómo. Por tanto, luce el distintivo declarativo.

Además de ser más clara y concisa, la función map puede optimizarse libremente sin necesidad de modificar nuestro valioso código de aplicación.

Para quienes piensen "Sí, pero el bucle imperativo es más rápido", sugiero investigar cómo el JIT optimiza el código. Aquí tienen un video esclarecedor

Otro ejemplo:

js
// imperative
const authenticate = (form) => {
  const user = toUser(form);
  return logIn(user);
};

// declarative
const authenticate = compose(logIn, toUser);

Aunque la versión imperativa no tiene errores, contiene una secuencia de evaluación codificada. La expresión compose simplemente declara un hecho: La autenticación es la composición de toUser y logIn. Nuevamente, esto permite flexibilidad en cambios de código auxiliar y eleva nuestro código a una especificación de alto nivel.

En el ejemplo anterior sí se especifica el orden de evaluación (toUser debe llamarse antes de logIn), pero existen escenarios donde el orden es irrelevante, fácilmente expresables mediante codificación declarativa.

Al no codificar el orden de evaluación, la programación declarativa facilita la computación paralela. Combinado con funciones puras, esto hace de FP una excelente opción para entornos paralelos/futuros, sin necesidad de código especial para lograr paralelismo.

Un Flickr de Programación Funcional

Construiremos una aplicación ejemplar usando enfoque declarativo y componible. Aunque temporalmente usaremos efectos secundarios, los minimizaremos y separaremos de nuestro código puro. Crearemos un widget que muestra imágenes de Flickr. Comencemos con el esqueleto HTML:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flickr App</title>
  </head>
  <body>
    <main id="js-main" class="main"></main>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

Esqueleto de main.js:

js
const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN('ramda/0.21.0/ramda.min');
const jquery = CDN('jquery/3.0.0-rc1/jquery.min');

requirejs.config({ paths: { ramda, jquery } });
requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
  // app goes here
});

Usamos ramda en lugar de lodash. Incluye compose, curry, etc. Requirejs podría parecer excesivo, pero garantiza consistencia en el libro.

Especificación: Nuestra aplicación hará 4 cosas:

  1. Construir URL para término de búsqueda
  2. Hacer llamada API a Flickr
  3. Transformar JSON resultante en imágenes HTML
  4. Mostrarlas en pantalla

Existen 2 acciones impuras identificadas: obtener datos de la API y renderizar en pantalla. Definámoslas primero para aislarlas. Agregamos función trace para depuración:

js
const Impure = {
  getJSON: curry((callback, url) => $.getJSON(url, callback)),
  setHtml: curry((sel, html) => $(sel).html(html)),
  trace: curry((tag, x) => { console.log(tag, x); return x; }),
};

Envuelve métodos jQuery en funciones currificadas con argumentos reordenados. El namespace Impure indica peligro. En futuros ejemplos las haremos puras.

Construyamos la URL para Impure.getJSON:

js
const host = 'api.flickr.com';
const path = '/services/feeds/photos_public.gne';
const query = t => `?tags=${t}&format=json&jsoncallback=?`;
const url = t => `https://${host}${path}${query(t)}`;

Existen formas complejas de escribir url pointfree con mónadas. Optamos por legibilidad usando estilo convencional.

Función app que ejecuta llamada y renderiza:

js
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');

Llama a url, pasa el string a getJSON (parcialmente aplicado con trace). Al cargar, la consola muestra respuesta API.

console response

Para construir imágenes del JSON: Las URLs están en items[i].media.m.

Usamos la función prop de Ramda para acceder propiedades anidadas. Versión simplificada:

js
const prop = curry((property, object) => object[property]);

Simplemente accede propiedades con []. Usémosla para obtener mediaUrls:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));

Tras obtener items, mapeamos para extraer URLs. Conectemos esto a la aplicación:

js
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);

Nueva composición llama a mediaUrls y actualiza el <main> HTML con ellas. Reemplazamos trace con render para mostrar URLs como imágenes.

Último paso: convertir URLs en etiquetas <img>. En aplicaciones grandes usaríamos React u otras bibliotecas. Para este caso, usemos jQuery.

js
const img = src => $('<img />', { src });

jQuery acepta arreglos de etiquetas en html(). Transformamos URLs en imágenes y las enviamos a setHtml.

js
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);

¡Listo!

cats grid

Script final: include

Esta especificación declarativa elegante define el «qué», no el «cómo». Cada línea representa una ecuación con propiedades que se cumplen, permitiendo razonamiento y refactorización.

Refactorización con Principios

Optimización posible: mapeamos items a URLs y luego a imágenes. Ley de composición de map: map(f ∘ g) == map(f) ∘ map(g)

js
// map's composition law
compose(map(f), map(g)) === map(compose(f, g));

Aplicaremos esta propiedad para optimizar:

js
// original code
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);

Alineamos los map. Gracias al razonamiento ecuacional, integramos mediaUrls en images:

js
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));

Aplicando ley de composición:

js
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/

const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));

Ahora solo itera una vez. Mejoremos legibilidad extrayendo la función:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));

Resumen

Hemos aplicado nuestras habilidades en una aplicación real. Usamos marco matemático para razonar y refactorizar. ¿Pero qué pasa con el manejo de errores y ramificación? ¿Cómo lograr que toda la aplicación sea pura en lugar de solo agrupar funciones destructivas en namespaces? ¿Mayor seguridad y expresividad? Temas de la Parte 2.

Capítulo 07: Hindley-Milner y Yo