Programación imperativa vs programación declarativa

August 15, 2019

...

Una de las cosas que me llamó la atención es que la cantidad de posts que te llevan a definiciones vagas u opiniones que se encuentran totalmente alejadas de la realidad abundad en internet sobre este tema, todos muestran el mismo ejemplo de la instrucción for y dicen exactamente lo mismo: Programación declarativa es cuando declaras lo que quieres hacer y no le dices como hacerlo. El problema de esta definición es que te lleva a la interpretación, y con la interpretación llega la opinión, y es en ese preciso momento donde el sentido que tiene el desarrollo, de ser una ciencia exacta, se pierde. No quiero ser mal interpretado con esto, las opiniones son validas y tu puedes preferir un estilo sobre el otro, lo que sencillamente no tolero es inventar definiciones. Me he llevado sorpresas del tipo: Un if es lo más declarativo que existe, ya que declaras que quieres que el código vaya por un lado o por el otro. Lo anterior es, académicamente, incorrecto. Programación declarativa y programación imperativa tienen una definición muy establecida y fácil de entender que no debería dar espacio para interpretaciones. Y en esta web vamos a ver esto.

Partamos por la más sencilla, la que te enseñan en todos los tutoriales de internet y también en las universidades:

Que es la programación imperativa?

Es un paradigma de programación, esta enfocada en cómo se ejecuta el código, define el control de flujo de manera explícita y cambia el estado de una máquina.

Ahora vamos parte por parte, la oración de esta enfocada en cómo se ejecuta el código esta abierta a la interpretación y le puedes dar la definición que quieras en base a que sea para ti como se ejecuta el código. Y por el solo hecho de ser ambigua la detesto. Sin embargo todo lo que viene después no es ambiguo ya que esta hablando sobre conjunto de operaciones que se realizan en los lenguajes de programación. Cuando se dice que define el control de flujo tenemos algo bastante detallado a que se refiere. Si buscamos en internet, lo cual no es necesario ya que te lo diré acá, podemos ver de que control de flujo se refiere a algo muy específico:

Definición de variables

Todo let, const o var es imperativo, cada vez que defines una constante estas definiendo control de flujo y, además, estás mutando el estado de la máquina, este puede ser después recolectado por el garbage collector, sin embargo sigue siendo una mutación que se encuentra en la memoria de la máquina y además define una variable que será utilizada más adelante para definir el flujo.

Decisiones

Todo if, else, else if, switch o instrucción que te permita a ti tomar una decisión en base a algo que ya definiste es considerado imperativo.

Loops

Así es, este aparece en todos los ejemplos de internet, todo loop controlado, ya sea for, while, do while, entre otros, donde donde en base a una condición debamos detener este loop, es considerado imperativo, incluso los foreach donde de manera explicita estemos indicando que vamos a iterar un arreglo, no así map, filter y reduce ya que son conceptos matemáticos de aplicar función, filtrar y finalmente fold, sobre esto hablaremos más adelante cuando veamos Monads. En definitiva cualquier loop controlado, infinito, con condición de salida, sin condición de salida, break o continue son considerados imperativos.

Manejo de excepciones con try catch

Cuando definimos de manera explicita un try catch, estamos definiendo de manera imperativa por que lado de nuestro código vaya la ejecución de este.

Async

Así es, nuestra querida función de Async await también es considerada imperativa.

Generadores

Funciones generadores también son consideradas parte del control de flujo y, por ende, imperativa.

Luego de haber visto esto lo más probable es que me estes odiando o estes de acuerdo conmigo, sin embargo déjame decirte de que todo lo escrito anteriormente no refleja mi opinión si no la misma definición que puedes encontrar en papers históricos que hablan sobre los primeros computadores y lenguajes de programación. Estamos hablando de que lo más probable es que hayas estado escribiendo código imperativo creyendo que este es código declarativo. Ahora que ya sabemos que es control de flujo nos queda ver la parte de "Cambia el estado de la máquina", sin embargo esto ya es más sencillo de ver.

Cambia el estado de la máquina

Con esto nos referimos a la declaración de variables, estas vas a modificar el estado de la máquina, y es aún peor si definimos variables globales ya que estas mutan el estado de la maquina y esta mutación puede perdurar durante bastante tiempo, años inclusive.

Programación declarativa

Ya definido el control de flujo y la programación imperativa ya podemos pasar a ver la definición de la programación declarativa:

Define la lógica de lo que se va a ejecutar, pero sin indicar como ni un detalle del control de flujo.

Esta definición, luego de que ya sabemos lo que es control de flujo, es muy poderosa, ya que te permite darte cuenta de que si has escrito un if, pensando que es declarativo, no estabas en lo correcto, la programación declarativa se ve muy parecida a un "tubo", donde se recibe una entrada y esta escupe una salida, y puedes puedes hacer código declarativo de multiples maneras, ya sea escribiendo solo middlewares, composición de funciones como vimos un capítulo anterior, con clases, etc. Nosotros no utilizaremos las clases pero si haremos código más declarativo utilizando composición o middlewares.

Un ejemplo de programación declarativa:

// la siguiente función es como compose, pero nos permite
// indicar un método con el cual vayamos a encadenar
// nuestras funciones
const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
);

// con esto podemos componer promesas!
const composeP = composeM('then');

const handleUserCreate =
  composeP(
    saveInDatabase(UserModel),
    validateWithSchema(userSchema),
  )

const app.post('/users', (req, res) =>
  handleUserCreate(req.body)
    .then(sendSuccess(res))
    .catch(sendError(res))
)

En el ejemplo de arriba vemos cual es la intención del código escrito:

  1. Tomamos el objeto de request y se lo entregamos a nuestra función handleCreateUser.
  2. La función valida la petición contra un esquema
  3. Luego guardamos en la base de datos con el modelo del usuario.
  4. Finalmente enviamos los datos al cliente o enviamos el error en el caso de que exista.

Si se fijan, el detalle de cómo esta implementado esto no lo vemos, tampoco nos interesa, lo que nos interesa es la intención del código. Sabiendo la intención del código podremos ver si existe algún error en nuestra lógica y, lo que finalmente nos interesa que se encuentre con tests, son estas pequeñas funciones que hemos creado donde nuestro detalle queda abstraído de nosotros. No hay if, try catch, for, async await, solo composición de funciones. Esto podría ser generalizado aún más y así poder reutilizar más nuestro código, incrementando nuestra velocidad y calidad en el largo plazo.

Así que la próxima vez que quieran escribir una funcionalidad, donde quieran plasmar la lógica de lo que se esta escribiendo, primero denle nombre a las funciones, definan paso a paso que es lo que quieren que haga su código, las dependencias que este puede necesitar utilizando currying, aplicación parcial o closures y, cuando el orden y lo que se tenga que ejecutar les haga sentido, luego implementen el detalle, o incluso mejor, los tests y luego el detalle.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.