Saltar al contenido
Explorar conocimiento

concepto · fundamentos · 13 min de lectura

Programación Declarativa

La programación declarativa describe qué resultado se quiere obtener sin especificar cómo obtenerlo, delegando el control de flujo al motor subyacente para ganar claridad y reducir errores.

No requiere conocimientos previos.

Alcance en breve

Cubre

  • declarative programming as describing what result to produce rather than how to produce it
  • examples across domains: SQL, HTML, CSS, regex, JSX, Terraform, GitHub Actions YAML
  • the relationship between declarative style and domain-specific languages
  • contrast with imperative programming
  • how functional programming inherits declarative properties

No cubre

  • formal logic programming (Prolog, Datalog) beyond a brief mention
  • constraint solving, theorem proving, and answer set programming
  • implementation details of declarative engines and compilers

Supone

  • The reader understands the difference between 'how' and 'what' in everyday tasks and has written or read code in any language.

Resumen

La programación declarativa es un paradigma donde el código expresa la lógica del problema sin describir el flujo de control. En vez de escribir instrucciones paso a paso que modifican estado, decís qué querés y dejás que el motor —el compilador de SQL, el renderizador de HTML, el motor de regex— descubra cómo obtenerlo.

Importa porque produce código más corto, más legible y con menos espacio para errores de implementación. Cuando escribís SELECT name FROM users WHERE age > 18, no te importa si el motor de base de datos usa un índice B-tree, un barrido secuencial o un bitmap scan. Solo te importa que el resultado sea correcto. Esa separación entre qué y cómo es la esencia del paradigma declarativo.

La mayoría de los lenguajes que usamos a diario no son puramente declarativos: son multi-paradigma. Pero los lenguajes específicos de dominio —SQL, HTML, CSS, regex, Terraform, GitHub Actions YAML— sí lo son, y su longevidad y ubicuidad son la mejor prueba de que el enfoque declarativo funciona.

Alcance y supuestos

Este paquete cubre la programación declarativa como paradigma: qué significa separar el «qué» del «cómo», ejemplos concretos en distintos dominios (consultas, interfaces, infraestructura, validación), y el contraste con la programación imperativa. También conecta la programación funcional con la declarativa: la FP es un subconjunto declarativo donde las funciones puras reemplazan las instrucciones paso a paso.

No cubre programación lógica (Prolog, Datalog), resolución de restricciones ni demostración automática de teoremas. Tampoco cubre los internals de motores declarativos (cómo SQL Server optimiza un plan de ejecución o cómo React reconcilia el DOM virtual).

Asume que entendés la diferencia entre «decir lo que querés» y «dar instrucciones detalladas» en la vida cotidiana, y que has escrito o leído código en al menos un lenguaje.

Modelo mental

Pedir comida en un restaurante versus cocinar en casa.

En casa (imperativo): «Abrí la heladera, sacá dos huevos, cascalos en un bowl, batilos, prendé la hornalla, poné la sartén, esperá a que caliente, verté los huevos, mové con la espátula, apagá el fuego, serví en un plato.»

En el restaurante (declarativo): «Huevos revueltos, por favor.» No le explicás al chef cómo batir los huevos ni a qué temperatura calentar la sartén. Describiste el resultado deseado y delegaste la ejecución.

Esa delegación no es magia: hay alguien —el chef, el motor de SQL, el renderizador del navegador— que sabe cómo ejecutar. La programación declarativa funciona porque existe una capa de abstracción poderosa entre tu intención y la máquina. Cuando esa capa es sólida, ganás productividad y perdés ruido. Cuando falla —por ejemplo, si el optimizador de SQL elige un mal plan de ejecución— necesitás entender el «cómo» para diagnosticar.

Uso práctico

El paradigma declarativo no es algo que «aplicás» como un patrón de diseño. Es una propiedad que emerge cuando usás las herramientas correctas para cada dominio:

  • ✅ Consultas a bases de datos: SQL es el ejemplo canónico. Describís columnas, tablas y condiciones; el motor decide el plan de ejecución.
  • ✅ Interfaces de usuario: HTML describe estructura (<h1>, <ul>, <article>) y CSS describe presentación. Decís «esto es un título» y «este elemento es rojo», no cómo renderizarlo.
  • ✅ Infraestructura como código: Terraform y CloudFormation describen el estado deseado. Decís «quiero una instancia EC2 con estas propiedades», no los pasos de la API para crearla.
  • ✅ Validación y parsing: regex describe un patrón. JSON Schema describe la forma esperada de un documento.
  • ✅ Pipelines CI/CD: GitHub Actions YAML describe cuándo y con qué disparar jobs, no cómo el runner los ejecuta.
  • ❌ Algoritmos con lógica de negocio compleja y específica: si el dominio no tiene un DSL maduro, forzar un enfoque declarativo suele terminar en un «framework interno» que solo entiende quien lo escribió.
  • ❌ Sistemas donde el rendimiento depende de decisiones finas sobre memoria y orden: ahí necesitás el control que da el estilo imperativo.

Declarativo en acción: cinco dominios, un patrón

El mismo principio —«describo qué quiero, no cómo»— se manifiesta en herramientas que usás todos los días.

SQL: describís el conjunto de datos que querés, no cómo recorrer las tablas.

SELECT name, SUM(amount) as total
FROM orders
WHERE status = 'delivered'
GROUP BY name
HAVING SUM(amount) > 100
ORDER BY total DESC;

No hay bucles, no hay índices, no hay variables temporales. El motor de base de datos decide si usa un hash join, un nested loop o un merge join basándose en estadísticas que vos ni conocés.

HTML + CSS: describís estructura y presentación.

<article>
  <h1>Título</h1>
  <p class="lead">Un párrafo destacado.</p>
</article>
.lead {
  font-size: 1.25rem;
  color: #333;
}

No le decís al navegador cómo calcular el layout, cómo medir el texto ni cómo pintar los píxeles. Describís la intención y el motor de renderizado hace el resto.

Regex: describís un patrón, no cómo matchearlo.

/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

Esa expresión valida emails. No escribiste un bucle que recorra caracteres, compare códigos ASCII y verifique posiciones del arroba. Describiste la forma que debe tener un string para ser válido.

Terraform (HCL): describís infraestructura deseada.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  tags = {
    Name = "web-server"
  }
}

No especificás las llamadas a la API de AWS, el polling hasta que la instancia esté lista ni el rollback si algo falla. Terraform calcula el plan —lo que hay que crear, modificar o destruir— y lo ejecuta.

GitHub Actions YAML: describís condiciones de ejecución, no scripts de orquestación.

on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

Decís «cuando pusheen a main, ejecutá estos pasos en ubuntu». No escribiste el código que escucha webhooks, clona repositorios, gestiona colas de runners ni limpia entornos efímeros.

La conexión con la programación funcional

La programación funcional es, en cierto sentido, un subconjunto declarativo del código de aplicación general. map, filter y reduce describen transformaciones deseadas sin exponer el mecanismo de iteración:

// Imperativo: cómo iterar
const result: string[] = [];
for (let i = 0; i < items.length; i++) {
  if (items[i].active) {
    result.push(items[i].name.toUpperCase());
  }
}

// Declarativo/FP: qué resultado
const result = items
  .filter(item => item.active)
  .map(item => item.name.toUpperCase());

La versión funcional no dice «inicializá un array vacío, recorré con índice, chequeá una condición, transformá, pusheá». Dice «filtrame los activos y transformame sus nombres a mayúscula». El motor de JavaScript sigue ejecutando instrucciones imperativas por debajo, pero vos no las escribiste. Eso es programación declarativa.

Ejemplo trabajado: dos enfoques para validar un formulario

Un sistema necesita validar datos de registro: el nombre no puede estar vacío, el email debe tener formato válido y la edad debe ser mayor o igual a 18. El equipo compara dos enfoques.

Enfoque imperativo: cada regla se codifica como una secuencia de comprobaciones con early returns.

interface UserInput {
  name: string;
  email: string;
  age: number;
}

function validate(input: UserInput): string[] {
  const errors: string[] = [];

  if (!input.name || input.name.trim().length === 0) {
    errors.push("Name is required");
  }
  if (!input.email || !input.email.includes("@")) {
    errors.push("Email is invalid");
  }
  if (input.age < 18) {
    errors.push("Must be at least 18 years old");
  }

  return errors;
}

Enfoque declarativo: las reglas se expresan como datos, y el motor de validación las aplica.

type Rule = {
  field: string;
  test: (value: unknown) => boolean;
  message: string;
};

const rules: Rule[] = [
  { field: "name", test: (v) => typeof v === "string" && v.trim().length > 0, message: "Name is required" },
  { field: "email", test: (v) => typeof v === "string" && v.includes("@"), message: "Email is invalid" },
  { field: "age", test: (v) => typeof v === "number" && v >= 18, message: "Must be at least 18" },
];

function validate(input: Record<string, unknown>, rules: Rule[]): string[] {
  return rules
    .filter(rule => !rule.test(input[rule.field]))
    .map(rule => rule.message);
}

Las diferencias prácticas:

  • Agregar una regla nueva en la versión imperativa es escribir otro bloque de if y acordarte de pushear al array de errores. En la declarativa es agregar un objeto al array rules, sin tocar la lógica de validación.
  • Reutilizar en otro formulario en la imperativa implica copiar y pegar. En la declarativa, pasás un array de reglas distinto a la misma función validate.
  • Testear en la imperativa requiere cubrir cada combinación de condiciones. En la declarativa, testeás las reglas individuales como datos puros y la función validate una sola vez.

La versión declarativa no eliminó la lógica imperativa: la movió a la función validate, que es genérica y no cambia. Las reglas —lo específico del dominio— quedaron como datos declarativos. Ese patrón —extraer el «cómo» a una función reusable y expresar el «qué» como configuración— es la estrategia más práctica para introducir estilo declarativo en cualquier código.

Para decidir si un enfoque declarativo suma o resta claridad:

  1. ¿Existe ya una herramienta o DSL declarativo maduro para este dominio (SQL, HTML, regex, Terraform)?
  2. ¿La lógica del dominio cambia más seguido que el mecanismo que la ejecuta?
  3. ¿Expresar el «qué» como configuración o reglas haría el código más fácil de modificar que codificar el «cómo» cada vez?

Si la respuesta es sí, buscá la capa declarativa que separe intención de implementación. Si no existe una herramienta madura y el dominio es demasiado específico, codificar el «cómo» directamente puede ser más claro que inventar un mini-lenguaje que solo entiende tu equipo.

Evidencia

  • Wikipedia, Declarative programming define el paradigma, lo contrasta con el imperativo y lista las subcategorías (funcional, lógico, constraint) con ejemplos de cada una.
  • Wikipedia, Imperative programming proporciona el contrapunto necesario: la sección sobre la relación entre paradigmas imperativos y declarativos contextualiza por qué coexisten en los lenguajes modernos.

Fuentes citadas