Back-end25+ minute read

La Programación Declarativa ¿De Verdad Existe?

En pocas palabras, programación declarativa consiste en decirle a un programa lo que tiene que hacer en lugar de decirle cómo debería hacerlo. Este enfoque significa implica proporcionar un lenguaje específico de dominio (DSL) para expresar lo que el usuario quiere. El DSL resguarda a los usuarios de construcciones de bajo nivel, al tiempo que materializa el estado final deseado.

Aunque la programación declarativa tiene sus ventajas sobre el enfoque imperativo que reemplaza, no es tan directa como parece. En este exhaustivo artículo, Federico Pereiro, Ingeniero Freelance de Software de Toptal, nos hace saber su experiencia con herramientas declarativas y explica cómo puedes hacer la programación declarativa sirva para ti también.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

En pocas palabras, programación declarativa consiste en decirle a un programa lo que tiene que hacer en lugar de decirle cómo debería hacerlo. Este enfoque significa implica proporcionar un lenguaje específico de dominio (DSL) para expresar lo que el usuario quiere. El DSL resguarda a los usuarios de construcciones de bajo nivel, al tiempo que materializa el estado final deseado.

Aunque la programación declarativa tiene sus ventajas sobre el enfoque imperativo que reemplaza, no es tan directa como parece. En este exhaustivo artículo, Federico Pereiro, Ingeniero Freelance de Software de Toptal, nos hace saber su experiencia con herramientas declarativas y explica cómo puedes hacer la programación declarativa sirva para ti también.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Federico Pereiro
Verified Expert in Engineering
12 Years of Experience

Maker of minimalist software. Designs, writes, tests, deploys and maintains real systems solving real (if boring) problems.

Read More
Share

La programación declarativa es en la actualidad, el paradigma dominante de un conjunto de dominios diversos y extensivos como: bases de datos, plantillas y gestión de configuración.

En pocas palabras, programación declarativa consiste en decirle a un programa lo qué tiene que hacer en lugar de decirle cómo debería hacerlo. En la práctica, este enfoque significa implica proporcionar un lenguaje específico de dominio (DSL, del inglés Domain-specific lenguage) para expresar lo qué el usuario quiere y resguardarlos de las arquitecturas o construcciones de bajo nivel (bucles, condicionales, asignaciones) que materializan el estado final deseado.

Mientras que este paradigma es una gran mejora sobre el enfoque imperativo que reemplazó, yo podría decir que la programación declarativa tiene unas limitaciones significativas, limitaciones que pienso explorar en este artículo. Además propongo un enfoque doble que engloba los beneficios de la programación declarativa, al tiempo que supera sus limitaciones.

CAVEAT: Este artículo se escribió como resultado de un multi-año de lucha personal con herramientas declarativas. Muchas de las cosas que argumentó no han sido probadas en su totalidad y algunas son presentadas como valor nominal. Una crítica apropiada sobre la programación declarativa tomaría bastante tiempo, esfuerzo y tendría que volver atrás y usar muchas de estas herramientas pero mi corazón no estaría en el esfuerzo. El punto de este artículo, es compartir mis pensamientos contigo, sin ajetreos, sólo mostraré lo que funcionó para mí. Si te has luchado con altibajos con herramientas de programación declarativa entonces tal vez encuentres consuelo y algunas alternativas. Y si te gusta este paradigma y sus herramientas, no me hagas mucho caso.

Si la programación declarativa funciona para ti no tengo derecho a decirte lo contrario.

Puedes odiar o amar la programación declarativa pero no puedes darte el lujo de ignorarla.

Puedes odiar o amar la programación declarativa pero no puedes darte el lujo de ignorarla.

Los Méritos De La Programación Declarativa

Antes de explorar las limitaciones de la programación declarativa, me parece es necesario entender sus méritos.

Sin duda la herramienta de programación declarativa más efectiva es la base de datos relacional (RDB, por su nombre en inglés relational database). Incluso podría considerarse la primera herramienta declarativa. De todos modos, RDB tiene dos propiedades que consideró son arquetípicas de la programación declarativa:

  • Un Lenguaje Específico de Dominio (DSL): la interface universal para bases de datos relacional es este DSL: Lenguaje Query Estructurado, mejor conocido como SQL.
  • El DSL esconde las capas de bajo nivel al usuario: desde que Edgar F. Codd publicó su artículo sobre RDB se ha vuelto bastante claro que el punto de este modelo es desasociar las consultas deseadas de los bucles, índices y rutas de acceso subyacentes que las implementan.

Antes de que existiera RDB la mayoría de los sistemas de base de datos podían ser accedidos a través de código imperativo, el cual es increíblemente dependiente de detalles de bajo nivel como records, índices y las rutas físicas hacia los propios datos. Debido a que estos elementos cambian después de cierto tiempo, es posible que el código deje de funcionar por algún cambio subyacente en la estructura de los datos. El código que te queda como resultado es difícil de escribir, de depurar, de leer y difícil de mantener. Es probable que el código, en su mayoría, fuera muy largo, estuviera lleno de condicionales muy largos, repeticiones y tal vez algunos bugs que no se notaban pero era dependientes del estado.

Si este era el caso, los RDB proveyeron un salto en la productividad muy grande para los desarrolladores de sistemas. Ahora, en lugar de tener miles de líneas de código imperativo puedes tener un esquema de datos definidos y además, cientos (o incluso unas pocas) de consultas. Como resultado, las aplicaciones solo tienen que lidiar con una representación abstracta, significativa y duradera de los datos; e interconectándola a través de un poderoso y a la vez sencillo lenguaje de consulta. La RDB probablemente ayudó a aumentar la productividad de programadores al igual que a las compañías que les daban trabajo, en un orden de magnitud.

¿Cuáles son las ventajas más comunes de la programación declarativa?

 Los defensores de la programación declarativa siempre mencionan rápidamente sus ventajas. Sin embargo, incluso estas personas admiten que viene con ciertos compromisos.

Los defensores de la programación declarativa siempre mencionan rápidamente sus ventajas. Sin embargo, incluso estas personas admiten que viene con ciertos compromisos.
  1. Legibilidad/usabilidad: un DSL usualmente se acerca más a un lenguaje natural (como el Inglés o Español) que a un pseudocódigo y por ello es más fácil de leer y aprender por personas que no son programadores.
  2. Concisión: la mayor parte de la tablatura es extraída por el DSL, dejando así menos líneas para hacer el mismo trabajo.
  3. Reutilización: es más fácil crear un código que puede ser usado para diferentes propósitos; algo que todos saben es extremadamente difícil cuando se usan construcciones imperativas.
  4. Idempotencia: puedes trabajar con estados finales y dejar que el programa haga el resto. Por ejemplo, en una operación de upsert puedes insertar una fila si no está ahí o puedes modificarla si ya se encuentra ahí, esto en lugar de escribir un código que se encargue de ambos casos.
  5. Error de Recuperación: es fácil identificar una construcción que se detendrá ante el primer error, en lugar de tener que añadir listados de errores para cada posible error. (Si alguna vez has escrito 3 retrollamadas o callbacks anidadas en node.js entonces sabes de lo que habló.)
  6. Transparencia Referencial: aunque esta ventaja se asocia más a la programación funcional, la verdad es que es válida para cualquier enfoque que minimice la manipulación manual del estado y se base en efectos secundarios.
  7. Conmutatividad: la posibilidad de expresar un estado final sin que se especifique el orden real en el que se implementará.

A pesar de que las ventajas mencionadas son muy comunes cuando se habla de programación declarativa, voy a resumirlas en dos cualidades que servirán como principios guía cuando proponga un enfoque alternativo. While the above are all commonly cited advantages of declarative programming, I would like to condense them into two qualities, which will serve as guiding principles when I propose an alternative approach.

  1. Capa de alto nivel adaptada a un dominio específico: la programación declarativa crear una capa de alto nivel usando la información del dominio al que se aplica. Definitivamente si estamos usando una base de datos es porque queremos un set de operaciones para manejar datos. La mayoría de las 7 ventajas que se mencionaron anteriormente vienen de la creación de una capa de alto nivel que fue diseñada especialmente para un problema de dominio.
  2. Poka-yoke (a prueba de errores): una capa de alto nivel adaptada a un dominio esconde los detalles imperativos de la implementación. Lo cual significa que cometerás menos errores porque los detalles de bajo nivel del sistema no son accesibles. Esta limitación elimina muchas clases de errores de tu código.

Dos Problemas con la Programación Declarativa

En las próximas dos secciones, expondré dos de los problemas principales de la programación declarativa: separación y falta de despliegue. Cada crítica necesita un bogeyman y es por ello que usaré una plantilla de sistema HTML como un ejemplo exacto de las deficiencias de la programación declarativa.

El Problema Con Las Separaciones De DSL

Imagina que necesitas escribir una aplicación web con un número de visitas nada trivial. Codificar estas visitas en un set de archivos HTML no es una opción, debido a que muchos componentes de estas páginas cambian.

La solución más rápida, la cual sería generar un HTML por medio de cadenas de concatenación, en realidad te parece horrible y empezarás a buscar un alternativa de inmediato. La solución estándar es usar un sistema de plantilla. Aunque existen diferentes tipos de sistemas de plantillas, de momento dejaremos de lado sus diferencias para este análisis. Podemos considerarlos como similares en lo que respecta a los sistemas de plantillas para proveer una alternativa a la codificación de concatenación de las cadenas HTML, usando condicionales y bucles. Así como las RDB emergieron como alternativa al enlazamiento de bucles a través de registros de datos.

Supongamos que escogemos el sistema de plantilla estándar; en este encontrarás tres fuentes de fricción, las cuales nombraré en orden ascendente dependiendo de su importancia. La primera sería que, la plantilla debe residir en un archivo separado de tu código. Ya que el sistema de plantilla usa un DSL, la sintaxis es diferente y por ello no puede estar en el mismo archivo. En proyectos sencillos, donde la cantidad de archivos no es alta, la necesidad de mantener archivos de plantilla por separado es dos veces o tres veces más importante debido a que supera la cantidad de archivos.

Haré una excepción con las plantillas Embedded Ruby (ERB de sus siglas en inglés Embedded Ruby templates) debido a que esas están integradas en el código fuente de Ruby. Pero este no es el caso para herramientas inspiradas en ERB, escritas en otros lenguajes ya que esas plantillas deben ser guardadas en archivos diferentes.

La segunda fuente de fricción es que el DSL tiene su propia sintaxis, una que es diferente a la del lenguaje de programación. Es por esto que modificar el DSL (e incluso escribir uno por tu cuenta) es muchísimo más difícil. Para pasar por debajo cuerda y cambiar la herramienta tendrías que aprender sobre tokenización y parsing (análisis sintáctico), los cuales son considerados interesantes y desafiante pero muy difícil. En mi opinión esto es una desventaja.

¿Cómo se puede visualizar un DSL? No es fácil pero se podría decir que un DSL es una capa limpia y bonita sobre una construcción de bajo nivel.

¿Cómo se puede visualizar un DSL? No es fácil pero se podría decir que un DSL es una capa limpia y bonita sobre una construcción de bajo nivel.

Tal vez te preguntes: “¿Por qué demonios querrías modificar tu herramienta? Si estás realizando un proyecto estándar, una herramienta que esté bien escrita debería servirte.” Puede que sí o puede que no.

Un DSL jamás tiene todo el poder de un lenguaje de programación. Si lo tuviera entonces no sería un DSL sino un lenguaje de programación completo.

¿Pero no es ese el punto de un DSL? El hecho de no tener todo el control de un lenguaje de programación ya disponible y así podemos obtener una abstracción y además eliminar más fuentes de bugs. Tal vez sí. Sin embargo, la mayoría de los DSL comienzan de forma sencilla y gradualmente empiezan a incorporar un gran número de facultades de un lenguaje de programación hasta que se convierte en uno. Sistemas de plantilla son el ejemplo perfecto. Ahora veamos las características estándar de los sistemas de plantilla y cómo se correlacionan con las facultades de un lenguaje de programación:

  • Reemplazar texto dentro de una plantilla: sustitución de variable.
  • Repetición de una plantilla: bucles.
  • Evita imprimir una plantilla si una condición no se ajusta: condicionales.
  • Parciales: subrutinas.
  • Ayudantes: subrutinas (la única diferencia con los parciales es que los ayudantes pueden acceder el lenguaje de programación subyacente y darte la libertad de salir del DSL).

Cuando dicen que un DSL es limitado porque es simultáneamente codicia y rechaza el control de un lenguaje de programación, esto es directamente proporcional al hecho de que las características del DSL son mapeables a las características de un lenguaje de programación. En lo que respecta a SQL, este no es el caso porque la mayoría de las cosas que ofrece SQL no se parecen a lo que normalmente encontrarías en un lenguaje de programación. Y al otro lado del espectro encontramos sistemas de plantilla donde virtualmente cada característica está haciendo que el DSL converja hacia BASIC.

Ahora volvamos atrás y contemplemos estas tres fuentes de fricción por excelencia que se resumen en el concepto de separación. Ya que está separado, un DSL necesita estar localizado en un archivo por separado; es difícil modificarlo (y aún más difícil escribirlo) y (en ocasiones pero no siempre) necesita que añadas una por una, las características que te hacen falta de un lenguaje programación real.

La separación es un problema inherente de cualquier DSL sin importar lo bien diseñado que esté.

Y ahora un segundo problema de las herramientas declarativas, que es algo general pero no inherente.

Otro Problema: La Falta De Despliegue Lleva A La Complejidad

Si hubiese escrito artículo hace unos meses, esta sección se habría llamado La Mayoría De Las Herramientas Declarativas Son #@!$#@! Complejas Pero No Sé Por Qué. En el proceso de escribir este artículo encontré una mejor forma de expresarlo: La Mayoría De Las Herramientas Declarativas Son Más Complejas De Lo Que Deberían. En esta sección explicaré por qué. Para analizar la complejidad de una herramienta, me gusta proponer una medida llamada la margen de complejidad. El margen de complejidad es la diferencia entre resolver un problema dado con un herramienta o resolverlo en el nivel bajo (supuestamente código imperativo sencillo) que la herramienta está intentado reemplazar. Cuando la solución anterior es más compleja que la siguiente entonces nos encontramos con el margen de complejidad. Cuando digo más complejo me refiero a más líneas de código, un código que es más difícil de leer, de modificar y de mantener pero no necesariamente todos al mismo tiempo.

Ten en cuenta que no estamos comparando la solución de bajo nivel con la mejor herramienta que pueda haber, en lugar de ello se está comparando a ninguna herramienta. Esto se parece al principio médico de “Primero, no harás daño”.

Señales de una herramienta con un gran margen de complejidad:

  • Algo que debería tomar algunos minutos en describir en grandes detalles en términos imperativos tomará horas en codificar usando la herramienta, aun cuando sabes cómo usar la herramienta.
  • Empiezas a sentir como que estás trabajando alrededor de la herramienta en lugar de con ella.
  • Te encuentras con dificultades para resolver un problema sencillo que claramente pertenece al dominio de la herramienta que estás utilizando pero la mejor respuesta que encuentras en Stack Overflow lo único que describe es una alternativa.
  • Cuando este problema sencillo podría ser resuelto por una determinada característica (que no existe en la herramienta) y ves un problema de Github en la librería que presenta una gran discusión de dicha característica con varios +1 intercalados.
  • Cómo sientes a sentir la necesidad incontrolable de dejar la herramienta por completo y hacer todo por ti mismo dentro de un _ por- bucle_.

Tal vez me deje llevar por las emociones debido a que los sistemas de plantilla no son tan complicados pero este relativamente pequeño margen de complejidad no es un mérito de su diseño, en lugar de ello el dominio de aplicabilidad es bastante simple (recuerda que estamos generando HTML). Cada vez que el mismo enfoque es usado para un dominio más complejo (como la gestión de configuración) el margen de complejidad podría convertir tu proyecto rápidamente en un cenagal.

Ahora, existen excepciones donde no es completamente inaceptable que una herramienta sea algo más compleja que el nivel bajo que desea reemplazar; si la herramienta da un código que es más leíble, conciso y correcto puede valer la pena. Es un problema cuando la herramienta es varias veces más compleja que el problema que está reemplazando, esto es totalmente inaceptable. Brian Kernighan es famoso por haber declarado que: “Que controlar la complejidad es la esencia de la programación de computadora.” Si una herramienta lo único que hace es agregar una complejidad significativa a tu proyecto, ¿para qué usarla?

Y nos preguntamos, ¿por qué algunas herramientas declarativas son más complejas de lo necesario? Creo sería un error echarle la culpa a un mal diseño. Es una explicación tan general, un argumento tan ad-hominem hacia los autores de estas herramientas que no es justo. Tiene que haber otra explicación, una más acertada e informada.

¡Y ahora un poco de Origami! Una herramienta con una interfaz de alto nivel para un nivel bajo abstracto debe desplegar el nivel alto desde el de abajo.

¡Y ahora un poco de Origami! Una herramienta con una interfaz de alto nivel para un nivel bajo abstracto debe desplegar el nivel alto desde el de abajo.

Mi argumento es que cada herramienta que ofrece una interfaz de alto nivel para extraer un nivel bajo debe desplegar ese nivel más alto desde el de abajo. El concepto de desplegar fue presentado en la obra de arte de Christopher Alexander: “La Naturaleza del Orden” o The Nature of Order - en particular el Volumen II. Está (irremediablemente) fuera del alcance de este artículo (sin mencionar, mi entendimiento) resumir las implicaciones de este monumental trabajo sobre diseño de software; aunque creo que en los próximos años su impacto será enorme. También está fuera del alcance de este artículo ofrecer una definición detallada de los procesos de despliegue. Pero usaré el concepto de forma heurística.

Un proceso de despliegue consiste en crear una estructura sin negar la que existe. En cada paso, cada cambio (o diferenciación, para citar los términos de Alexander) se mantiene en armonía con cualquier estructura anterior cuando la estructura anterior es, simplemente, una secuencia cristalizada de cambios pasados.

Lo más interesante es que Unix es un excelente ejemplo del despliegue de un nivel superior desde uno abajo. En Unix, dos características complejas del sistema operativo, los trabajos por lote y las co-rutinas (tuberías) son simplemente extensiones de los comandos básicos. Por ciertas decisiones de diseño, como lo es hacer que todo sea un flujo de bytes, la Shell un programa para usuario y los archivos I/O sean estándar, Unix es capaz de proporcionar estas características tan sofisticadas como la menor complejidad. Para sobresaltar porque estos ejemplos de despliegue son excelentes, voy a citar algunos extractos de un artículo de 1979 de Dennis Ritchie, uno de los autores de Unix:

Sobre trabajos en lote:

…el nuevo esquema de proceso instantáneamente hizo que algunas características valiosas fueran triviales al momento de implementarlas; por ejemplo, un proceso separado (con &) y un uso recursivo de shell como comando. La mayoría de los sistemas tienen que suministrar alguna especie de aplicación de trabajo en lote una facultad y un comando de intérprete especial para archivos distintos de los que se usan interactivamente.

Sobre co-rutinas:

Lo más genial de las tuberías de Unix es precisamente que están construidas desde los mismos comandos usados constantemente de forma simple.

Los pioneros de Unix Dennis Ritchie y Ken Thompson crearon una gran demostración de despliegue en su sistema operativo. Y también nos salvaron de un futuro distópico con sólo Windows como opción.

Los pioneros de Unix Dennis Ritchie y Ken Thompson crearon una gran demostración de despliegue en su sistema operativo. Y también nos salvaron de un futuro distópico con sólo Windows como opción.

Esta elegancia y simplicidad, me parece, viene de un proceso de despliegue. Los trabajos en lote y las co-rutinas son desplegados de estructuras previas (los comandos funcionan en un shell del usuario). Pienso que debido a la filosofía minimalista y los recursos limitados del equipo que creó Unix, el sistema evolucionó de lado y como tal fue capaz de incorporar características avanzadas sin volverse a mirar las más básicas, porque no había suficientes recursos para hacerlo de otra forma.

En la ausencia de un proceso de despliegue, el nivel alto será considerablemente más complejo de lo necesario. Es decir, la complejidad de la mayoría de las herramientas declarativas se desarrolla del hecho de que su nivel más alto no se despliega del nivel bajo que intenta reemplazar.

Esta falta de desplegamiento, si omites el neologismo, es rutinariamente justificada por la necesidad de resguardar al usuario del nivel bajo. Este énfasis de utilizar un poka-yoke (proteger al usuario de los errores del nivel bajo) viene con un costo, el cual es un margen de complejidad que se autodestruye por la complejidad extra generará nuevas clases de errores. Y para completar, estas clases de errores no tienen nada que ver con el problema de dominio si no con la herramienta en sí. No habría exageración alguna si describiéramos estos errores como una iatrogenia.

Las herramientas de plantillas declarativas cuando se aplican a la tarea de generar vistas HTML, son un caso arquetípico de un nivel alto que rechaza un nivel bajo que pretende rechazar. ¿Cómo así? Por qué generar cualquier vista no-trivial requiere lógica y los sistemas de plantillas, especialmente los que menos lógicos, dejan de lado la lógica por completo y después tratan de meter un poco de esta cuando creen que nadie está viendo.

Nota: una justificación aún más pobre para un margen de complejidad es cuando tratan de vender una herramienta como mágica o algo que sencillamente funciona; la vaguedad del nivel bajo se supone que es algo positivo porque una herramienta mágica siempre funciona sin tu saber por qué o cómo lo hace. En mi experiencia, mientras más mágica parezca la herramienta más rápido trasmutará el entusiasmo a frustración.

¿Y qué pasa con la separación de inquietudes? ¿No deberían la vista y la lógica permanecer separadas? El error más común con esto, es asumir que la lógica de negocios y la de presentación son iguales. La lógica de negocios no tiene ningún sentido en una plantilla pero la lógica de presentación existe sin importar nada. Omitir la lógica de las plantillas pone de lado la lógica de presentación y la coloca en el servidor, en donde se debe adaptar de forma incómoda. Toda esta clara explicación se la debo a Alexei Boronine, quien presenta un caso muy bueno en este artículo.

A mi parecer, más o menos dos tercios del trabajo de la plantilla se encuentra en su lógica de presentación mientras que el otro tercio se encarga de lidiar con los problemas genéricos como los son: las cadenas de concatenación, las etiquetas cerradas, escapar de caracteres especiales y la lista sigue. Esta es la naturaleza doble-cara de generar vistas HTML. Los sistemas de plantilla se encargan cómo se debe de la segunda mitad pero no les va muy bien con la primera. Las plantillas sin lógica le dan la espalda a este tipo de problemas sin dudar, lo cual te obliga a ti a solucionar el problema. Otros sistemas de plantilla sufren porque en realidad necesitan proveer un lenguaje de programación no-trivial para que sus usuarios puedan escribir una lógica de presentación.

Para finalizar, las herramientas declarativas de plantillas sufren por lo siguiente:

  • Si se atrevieran a desplegar desde su problema de dominio, tendrían que proveer formas de generar patrones lógicos;
  • Un DSL que provee una lógica no es en realidad un DSL sino un lenguaje de programación. Cabe destacar que otros dominios, como la gestión de configuración, también sufre por falta de “desplegamiento.”

Cerraré esta crítica con un argumento que está lógicamente desconectado del hilo que trae este artículo pero que se acerca mucho a su núcleo sentimental: Tenemos poco tiempo para aprender. La vida es corta y aparte de eso, necesitamos trabajar. Al encontrarnos con nuestras limitaciones, necesitamos pasar nuestro tiempo aprendiendo nuevas cosas que serán útiles y servirán para nuestro tiempo, incluso cuando nos enfrentamos al rápido cambio tecnológico. Es por ello que te aconsejo a que uses herramientas que no solo proveen una solución pero de hecho sirven para el dominio al que se aplica. Las RDB te enseñan sobre datos y Unix te enseña sobre conceptos de sistema operativo pero con herramientas poco satisfactorias que no se despliegan, siempre he sentido que he estado aprendiendo las intrincaciones de una solución sub-óptima mientras me mantengo en la obscuridad sobre la naturaleza del problema que se pretende resolver.

La heurística que sugiero consideres ahora es ** valora las herramientas que iluminan su problema de dominio, en lugar de las herramientas que obscurecen su problema de dominio detrás de características presuntuosas**.

El Enfoque Gemelo

Para superar los dos problemas de la programación declarativa que he mencionado aquí, propongo un enfoque gemelo:

  • Utiliza un lenguaje específico de estructura de dominio de datos (o dsDSL, por sus siglas en inglés data structure domain specific language) para superar la separación.
  • Crea un nivel alto que se despliegue desde el nivel de abajo para así superar el margen de complejidad.

dsDSL

Una estructura de datos DSL (dsDSL) es un DSL que se construye con las estructuras de datos de un lenguaje de programación. La idea central es usar estructuras básicas de datos que tienes disponible, como lo son las cadenas, números, matrices, objetos y funciones, y luego combina todo para crear extracciones que puedan resolver un dominio específico.

Queremos mantener el control sobre las estructuras o acciones declaradas (de nivel alto) sin tener que especificar los patrones que implementan estas construcciones (de nivel bajo). Queremos superar la separación entre DSL y nuestro lenguaje de programación para que podamos ser libres de usar todo el poder de un lenguaje de programación cada vez que lo necesitemos. Esto no es solo posible pero un enfoque directo a través de las dsDSL.

Si hubieses preguntado hace un año, habría pensado que el concepto de una dsDSL era idealista, y luego me di cuenta que JSON es en realidad ¡un gran ejemplo de este enfoque! Un objeto de JSON analizado consiste en estructuras de datos que representan declarativamente entradas de datos para poder tener las ventajas del DSL y también hacerlo más fácil de analizar y manejar desde un lenguaje de programación. (Puede que existan otras dsDSL en el mundo pero hasta no he visto ninguna. Si tú conoces alguna me encantaría que la compartieras en la sección de comentarios.)

Al igual que JSON, una dsDSL tiene los siguientes atributos:

  1. Consiste en un conjunto pequeño de funciones: JSON tiene dos funciones principales parse y stringify.
  2. Sus funciones más comunes tienen argumentos complejos y recursivos: un JSON analizado es una matriz u objeto que usualmente contiene otras matrices u objetos dentro del mismo.
  3. Las entradas a estas funciones conforman representaciones específicas: JSON tiene un esquema de validación explícito y estrictamente forzado que te puede diferenciar estructuras válidas de las no válidas.
  4. Tanto las entradas como las salidas de estas funciones pueden ser retenidas y generadas por un lenguaje de programación sin necesidad de una sintaxis por separado.

Pero las dsDSL van más allá de JSON en muchos sentidos. Creemos una dsDSL para generar HTML usando Javascript. Luego hablaré sobre el problema de si este enfoque puede ser extendido a otros lenguajes (SPOILER: definitivamente se puede hacer en Ruby y Python pero probablemente no en C).

HTML es un lenguaje de marcado compuesto de etiquetas delimitadas por paréntesis angulares (< y >). Estas etiquetas pueden ser atributos y contenidos opcionales. Atributos son simplemente un listado de atributos clave/estimación y los contenidos pueden ser textos u otras etiquetas. Los atributos y los contenidos son opcionales para cualquier etiqueta. Estoy simplificando todo esto pero es bastante acertado.

Una forma muy directa de representar una etiqueta HTML en una dsDSL es usar una matriz con tres elementos: - Etiqueta: una cadena. - Atributos: un objeto (de tipo clave/estimación simple) o sin definir (si ningún atributo es necesario). - Contenidos: una cadena (texto), una matriz (otra etiqueta) o sin definir (si no hay ningún contenido).

Por ejemplo, <a href="views">Index</a> puede escribirse como ['a', {href: 'views'}, 'Index'].

Si queremos encajar este elemento de anclaje en un div con links de clase, podemos escribir: ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']].

Para enumerar varias etiquetas html en un mismo nivel, podríamos ponerlas en una matriz:

[
  ['h1', '!Hola!'],
  ['a', {href: 'views'}, 'Index']
]

El mismo principio se puede aplicar para crear varias etiquetas dentro de una etiqueta:

['body', [
  ['h1', '!Hola!'],
  ['a', {href: 'views'}, 'Index']
]]

Claro, está dsDSL no nos valdrá de mucho sino generamos un HTML desde la misma. Necesitamos una función generar que hará que nuestra dsDSL cree una cadena con HTML. Es por ello que si ejecutamos generar (['a', {href: 'views'}, 'Index']), obtendremos la cadena <a href="views">Index</a>.

La idea de cualquier DSL es especificar algunas construcciones con una estructura determinada que luego será pasada a una función. En este caso, la estructura que hace la dsDSL es esta matriz que tiene de uno a tres elementos; estas matrices tienen una estructura específica. Si generar de verdad valida su entrada (y es importante y también fácil validar por completo la entrada, ya que estas reglas de validación son la analogía precisa de la sintaxis de un DSL), esto te dirá exactamente en qué te equivocaste en tu entrada. Después de un tiempo comenzarás a reconocer lo que hace que una estructura sea válida en una dsDSL, y esta estructura será altamente sugestiva de lo que se genera de forma subyacente.

¿Cuáles son los méritos de una dsDSL en contraste a un DSL?

  • Una dsDSL es un parte integral de tu código. Ayuda a tener menos recuentos de líneas, de archivos y una reducción de todo en general.
  • Las dsDSL son fácil de analizar (y por ende más fácil de implementar y modificar). Parsing es sencillamente iterar a través de elementos de una matriz o un objeto. De igual forma las dsDSL son relativamente fácil de diseñar porque en lugar de crear una nueva sintaxis (algo que todo el mundo odiaría) puedes quedarte con la sintaxis de tu lenguaje de programación (algo que también es odiado pero al menos ya se sabe lo que es).
  • Una dsDSL tiene todo el control de un lenguaje de programación. Esto significa que una dsDSL cuando se usa de manera correcta tiene las ventajas de una herramienta de alto nivel al igual que la de una de bajo nivel.

Ahora tenemos el último argumento que es bastante fuerte, es por ello que haré énfasis en el mismo en esta sección. ¿A qué me refiero con empleado apropiadamente? Para ver cómo funciona esto, consideremos un ejemplo en el que queramos construir una tabla para mostrar la información de una matriz llamada Now, the last claim is a strong one, so I’m going to spend the rest of this section DATA.

var DATA = [
  {id: 1, descripción: 'Producto 1', precio: 20,  onSale: verdadero,  categorías: ['a']},
  {id: 2, descripción: 'Producto 2', precio: 60,  onSale: falso, categorías: ['b']},
  {id: 3, descripción: 'Producto 3', precio: 120, onSale: falso, categorías: ['a', 'c']},
  {id: 4, descripción: 'Producto 4', precio: 45,  onSale: verdadero,  categorías: ['a', 'b']}
]

En una aplicación real DATA será generada dinámicamente de una consulta de la base de datos.

Además tenemos una variable FILTER que cuando se inicialice será una matriz con categorías que queremos mostrar.

Queremos que nuestra tabla haga esto:

  • Muestre encabezados de tabla.
  • Para cada producto, se mostrarán los campos: descripción, precio y categorías.
  • No imprimir el campo id pero agregarlo como un atributo id para cada fila. VERSIÓN ALTERNATIVA: Agregar un atributo id para cada elemento tr.
  • Colocar una clase onSale si el producto está en oferta (on sale).
  • Ordenar los productos en orden descendiente con respecto al precio.
  • Filtrar ciertos productos por categoría. Si FILTER es una matriz vacía, mostraremos todos los productos. Y si no, mostraremos los productos donde la categoría del producto esté contenida dentro de FILTER.

Podemos crear la presentación lógica que coincida con este requerimiento en ~20 líneas de código:

function drawTable (DATA, FILTER) {
 
  var printableFields = ['description', 'price', 'categories'];
 
  DATA.sort (function (a, b) {return a.price - b.price});
 
  return ['table', [
     ['tr', dale.do (printableFields, function (field) {
        return ['th', field];
     })],
     dale.do (DATA, function (product) {
        var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) {
           return FILTER.indexOf (category) !== -1;
        });
 
        return matches === false ? [] : ['tr', {
           id: product.id,
           class: product.onSale ? 'onsale' : undefined
        }, dale.do (printableFields, function (field) {
           return ['td', product [field]];
        })];
     })
  ]];
}

Estoy consciente que esto no es ejemplo claro, sin embargo, sí representa una vista bastante simple de las 4 funciones básicas de almacenamiento persistente, también conocido como CRUD. Cualquier aplicación web no-trivial tendrá vistas que son más complejas que esto.

Ahora veamos lo que hace este código. Primero, define una función, drawTable para que contenga la presentación lógica de dibujar la tabla del producto. Esta función recibe DATA y FILTER como parámetros, es por ello que puede ser usada por diferentes sets de datos y por filtros. drawTable Tiene un doble papel, ser parcial y ayudante.

  var drawTable = function (DATA, FILTER) {

La variable interna, printableFields, es el único lugar donde necesitas especificar cuáles campos son imprimibles, para evitar repeticiones e inconsistencias al enfrentarse con cambios de requisitos.

  var printableFields = ['description', 'price', 'categories'];

Luego clasificamos la DATA de acuerdo al precio de sus productos. Ten en cuenta que algunos criterios de clasificación diferentes y más complejos serían más directos en implementación, ya que tenemos todo el lenguaje de programación a nuestra disposición.

  DATA.sort (function (a, b) {return a.price - b.price});

Aquí regresamos un objeto literal; una matriz que contiene una “tabla” como su primer elemento y sus contenidos como segundo. Esto es la representación dsDSL de la <table> que queremos crear.

  return ['table', [

Ahora creamos una fila con los encabezados de tabla. Para crear sus contenidos, usamos dale.do la cual es una función como Array.map, pero que también trabaja para objetos. Vamos a iterar printableFields y generar encabezados de tabla para cada uno de ellos:

     ['tr', dale.do (printableFields, function (field) {
        return ['th', field];
     })],

Te puedes dar cuenta que acabamos de implantar la iteración, la fuerza de trabajo de la generación de HTML, y no hubo necesidad de tener ninguna construcción DLS; solo necesitamos una función para iterar una estructura de datos y regresar las dsDSL. Un nativo similar o función implementada por el usuario hubiese tenido el mismo efecto.

Ahora debes iterar a través de los productos contenidos en DATA.

     dale.do (DATA, function (product) {

Comprobamos si este producto es rechazado por FILTER. Si FILTER está vacío, podemos imprimir el producto. Si FILTER no está vacío, vamos a iterar a través de categorías del producto hasta que encontremos uno que esté dentro de FILTER. Hacemos esto usando dale.stop.

        var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) {
           return FILTER.indexOf (category) !== -1;
        });

Nota la complejidad del condicional; está hecha a la medida de nuestros requisitos y tenemos completa libertad para expresarlo, porque estamos en un lenguaje de programación y no de DSL.

Si la coincidencia es falsa, se regresa una matriz vacía (así que no imprimimos este producto). De otro modo, se regresa un <tr> con su respectiva identificación y clase, al igual que iteramos a través de printableFields para, indudablemente, imprimir los campos.

 
        return matches === false ? [] : ['tr', {
           id: product.id,
           class: product.onSale ? 'onsale' : undefined
        }, dale.do (printableFields, function (field) {
           return ['td', product [field]];

Por supuesto cerramos todo lo que abrimos. ¿Es divertida la sintaxis, verdad?

        })];
     })
  ]];
}

Ahora, ¿cómo incorporamos esta tabla a un contexto más amplio? Escribimos una función llamada drawAll que invocará todas las funciones que generan las vistas. Aparte de drawTable, también podríamos tener drawHeader, drawFooter y otras funciones comparables, todas éstas regresarán dsDSLs.

var drawAll = function () {
  return generate ([
     drawHeader (),
     drawTable (DATA, FILTER),
     drawFooter ()
  ]);
}

Si no te gusta como se ve el código de arriba, nada de lo que diga te convencerá. Esto es una dsDSL en su mejor momento. Podrías en este momento dejar de leer el artículo (y también, dejar un comentario malvado, porque te has ganado el derecho de hacerlo si has llegado hasta este punto!). Pero en serio, si el código de arriba no parece elegante, nada en este artículo lo parecerá.

Para aquellos que todavía me siguen, me gustaría regresar a la declaración inicial de esta sección, la cual es qué una dsDSL tiene las ventajas de ambos niveles, alto y bajo:

  • La ventaja del nivel bajo está en escribir códigos cuando queramos, deshacernos de la camisa de fuerza del DSL.

  • La ventaja del nivel alto está en usar literales que representan lo que queremos declarar y dejar que las funciones de la herramienta conviertan eso en estado final deseado (en este caso, una secuencia con HTML).

¿Pero cómo es esto diferente de un código netamente imperativo? Yo pienso que la elegancia del acercamiento de la dsDSL baja al hecho de que el código escrito de esta manera, consiste mayormente, de expresiones en vez de declaraciones. Para ser más preciso, cualquier código que use una dsDSL está prácticamente compuesto de:

  • Literales que mapean hacia estructuras de más bajo nivel.
  • Invocaciones de función o lambdas dentro de esas estructuras literales que regresan estructuras de la misma clase.

Código que consiste mayormente en expresiones, el cual encapsula la mayoría de las declaraciones dentro de funciones, es extremadamente breve, porque todos los patrones de repetición pueden ser fácilmente abstraídos. Puedes escribir código arbitrario, siempre y cuando ese código regrese un literal que conforma a una forma no-arbitraria muy específica.

Otra característica de las dsDSL (la cual no se puede explorar mucho por ahora) es la posibilidad de usar tipos para incrementar el atractivo y la suculencia de las estructuras literales. En un próximo artículo haré énfasis en este punto.

¿Podría ser crear una dsDSL sin JavaScript, el Único y Verdadero Lenguaje? Sí, creo que sí es posible, siempre y cuando el lenguaje soporte:

  • Literales para: matrices, objetos (matrices asociativas), invocaciones de función y lambdas.
  • Detección de tipo de ejecución
  • Tipos de retorno dinámicos y polimorfismo

Creo que esto significa que las dsDSL son sostenibles en un lenguaje dinámico moderno (i.e.: Ruby, Python, Perl, PHP) pero probablemente no en C o Java.

Primero camina, luego deslízate: Cómo Desplegar Lo Alto Desde Lo Bajo

En esta sección intentaré mostrar una forma de desplegar una herramienta de nivel alto desde su dominio. En resumen, el enfoque consiste en estos pasos

  1. Agarra dos o cuatro problemas que sean representaciones de instancias de un problema de dominio. Estos problema debería ser reales. Desplegar el nivel alto desde el bajo es un problema de inducción, es por ello que necesitas datos reales para poder obtener una solución representativa.
  2. Resuelva los problemas sin una herramienta de la forma más rápida posible.
  3. Tomate un momento para ver tus soluciones y date cuenta de los patrones comunes que tienen las mismas.
  4. Encuentra los patrones de la representación (alto nivel).
  5. Encuentra los patrones de generación (nivel bajo).
  6. Resuelve los mismos problemas con tu capa de alto nivel y verifica que las soluciones sean correctas.
  7. Si ves que puedes representar fácilmente todos los problemas con tus patrones de representación, y los patrones de generación para cada una de estas instancias producen las implementaciones correctas, estás listo. Si no, regresa a la tabla de dibujar.
  8. Si aparecen nuevos problemas, resuélvelos con la herramienta y modifícalos como se debe.
  9. La herramienta debería converger asintóticamente a un estado final, sin importar cuantos problemas esté resolviendo. En otras palabras, la complejidad de una herramienta debería permanecer constante en lugar de crecer con la cantidad de problemas que resuelva.

¿Qué demonios son los patrones de representación y los patrones de generación? Qué bueno que preguntaste. Los patrones de representación son los patrones en los que deberías ser capaz de expresar un problema que pertenece a un dominio que concierne a tu herramienta. Es un alfabeto de estructuras que te permite escribir cualquier patrón que te gustaría expresar dentro del dominio aplicable. En un DSL, esto sería las reglas de producción. Ahora regresemos a nuestra dsDSL para generar un HTML.

La humilde etiqueta HTML es un buen ejemplo de los patrones de representación. Ahora veamos de cerca estos patrones básicos.

La humilde etiqueta HTML es un buen ejemplo de los patrones de representación. Ahora veamos de cerca estos patrones básicos.

Los patrones de representación para HTML son los siguientes:

  • Una etiqueta: ['TAG']
  • Una etiqueta con atributos: ['TAG', {attribute1: value1, attribute2: value2, ...}]
  • Una etiqueta con contenidos: ['TAG', 'CONTENTS']
  • Una etiqueta con atributos y contenidos: ['TAG', {attribute1: value1, ...}, 'CONTENTS']
  • Una etiqueta con otra etiqueta dentro: ['TAG1', ['TAG2', ...]]
  • Un grupo de etiquetas (solas o dentro de otras etiquetas): [['TAG1', ...], ['TAG2', ...]]
  • Dependiendo de cierta condición, pon o no una etiqueta: condition ? ['TAG', ...] : [] / Dependiendo de cierta condición, pon o no un atributo: ['TAG', {class: condition ? 'someClass': undefined}, ...]

Estas instancias pueden ser representadas con la notación dsDSL que determinamos en la sección anterior. Y esto es todo lo que necesitas para representar cualquier HTML que necesites. Patrones más sofisticados como una iteración condicional a través de un objeto para generar una tabla, podría ser implementada con funciones que devuelven los patrones de representación mencionados antes y estos patrones mapean directamente hacia etiquetas HTML.

Si los patrones de representación son las estructuras que se utilizan para expresar lo que quieres, los patrones de generación son las estructuras que tu herramienta usará para convertir los patrones de representación a estructuras de bajo nivel. Para HTML, se trata de las siguientes:

  • Validar la entrada (de hecho esto es un patrón de generación universal).
  • Abre y cierra las etiquetas (pero las etiquetas vacías como <entrada>, las cuales se cierran solas)
  • Coloca atributos y contenidos, escapando de caracteres especiales (pero no los contenidos de las etiquetas <style> y <script>)

Aunque no lo creas estos son los patrones que necesitas crear para desplegar una capa de dsDSL que genere HTML: Patrones similares puede encontrarse para generar CSS. De hecho lith hace ambos en ~250 líneas de código.

Y por último, ¿a qué me refiero con “camina, luego deslízate? Cuando nos encontramos con un problema de dominio, queremos usar una herramienta que nos salve de los terribles detalles de ese dominio. En otras palabras, queremos deshacernos del nivel bajo sin que nadie lo note, y mientras más rápido sea mejor. El enfoque “camina, luego deslízate” propone exactamente lo opuesto a esto: quédate más tiempo en el nivel bajo. Hazte amigo de sus rarezas y entiende cuales son esenciales y cuáles pueden ser evitadas cuando se presenten problemas reales, variados y útiles.

Después de caminar por algún tiempo en el nivel bajo y luego haya resuelto problemas útiles, entonces tendrás una comprensión más profunda de su dominio. Los patrones de representación y de generación saldrán a la luz naturalmente; estos derivan de la naturaleza de los problemas que intentan resolver. Luego puedes escribir el código que los usará. Si funcionan, serás capaz de deslizarte por los problemas donde recientemente habías tenido que caminar. Deslizarse significa muchas cosas; implica velocidad, precisión y falta de fricción. Tal vez esta cualidad se sienta más cuando estés resolviendo problemas con esta herramienta, ¿sientes que estás caminando en el problema, o sientes que te estás deslizando a través del mismo?

Tal vez la cosa más importante sobre una herramienta de despliegue no es el hecho que nos libera de tener que lidiar con el nivel bajo. Mejor dicho, al capturar los patrones empíricos de repetición en el nivel bajo, una buena herramienta de nivel alto nos permite entender por completo la aplicabilidad del dominio.

Una herramienta de despliegue no solo resolverá un problema - sino que también te ayudará a entender la estructura del problema.

Así que no huyas de un problema valioso. Primero camina por él y luego deslízate a través de él.

Hire a Toptal expert on this topic.
Hire Now
Federico Pereiro

Federico Pereiro

Verified Expert in Engineering
12 Years of Experience

Leiden, Netherlands

Member since November 4, 2015

About the author

Maker of minimalist software. Designs, writes, tests, deploys and maintains real systems solving real (if boring) problems.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.