DSL en Kotlin: cómo crear tu propio lenguaje de programación en una tarde

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

¿Qué es un DSL y por qué lo necesita?

\n\n

Imagine que no escribe código, sino una instrucción en un lenguaje comprensible que se convierte por sí sola en un programa funcional. Así es como funcionan los lenguajes específicos del dominio (DSL — Domain Specific Language). A diferencia de los lenguajes universales (Java, Python, C#), un DSL se crea para resolver una tarea concreta — ya sea la configuración de un servidor, la descripción de reglas de negocio o la construcción de consultas SQL complejas.

\n\n

Kotlin es el candidato ideal para crear DSL. Gracias a las lambdas con receptor, las extensiones y los constructores type-safe, puede escribir código que parece sintaxis nativa. En este artículo no solo veremos teoría, sino que crearemos un DSL real para un framework de pruebas — y verá lo sencillo y fascinante que es.

\n\n

¿Por qué Kotlin es el rey de los DSL?

\n\n

Kotlin se ha ganado el cariño de los desarrolladores no solo por su compatibilidad con Java. Sus capacidades sintácticas permiten crear DSL internos — lenguajes que viven dentro del lenguaje anfitrión y utilizan su sintaxis. Estos son los tres pilares sobre los que se sustenta cualquier DSL en Kotlin:

\n\n
    \n
  • Lambdas con receptor — permiten acceder a los métodos de un objeto sin indicar explícitamente su nombre. Esta es la base del patrón builder.
  • \n
  • Extensiones — añaden nuevas funciones a cualquier clase, incluso si no puede modificarla.
  • \n
  • Funciones infijas — permiten escribir código en un estilo similar al lenguaje natural: should be equalTo 5.
  • \n
\n\n

Estas capacidades convierten la creación de DSL en un juego de construcción Lego: toma bloques ya preparados — lambdas, funciones, propiedades — y ensambla con ellos un lenguaje claro y seguro.

\n\n

Creando un DSL simple para pruebas: paso a paso

\n\n

Escribamos un DSL para realizar pruebas. El objetivo es que las pruebas se vean como una descripción legible, no como un conjunto de llamadas a assert:

\n\n
test("La calculadora debe sumar números") {\n    val result = Calculator.add(2, 3)\n    result should beEqualTo 5\n    result shouldNot beNull\n}
\n\n

Reconozca que esto es casi inglés. Ahora analicemos cómo funciona.

\n\n

Paso 1. Crear el contexto de la prueba

\n\n

Necesitamos una función test que reciba un nombre y un bloque de código. El bloque se ejecuta en un contexto donde están disponibles should y shouldNot:

\n\n
class TestContext {\n    fun Any.should(matcher: Matcher) {\n        if (!matcher.matches(this)) {\n            throw AssertionError("Se esperaba ${matcher.expected}, pero se obtuvo $this")\n        }\n    }\n\n    fun Any.shouldNot(matcher: Matcher) {\n        if (matcher.matches(this)) {\n            throw AssertionError("No se esperaba ${matcher.expected}, pero se obtuvo $this")\n        }\n    }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n    println("Ejecutando prueba: $name")\n    try {\n        TestContext().block()\n        println("✓ Aprobada")\n    } catch (e: AssertionError) {\n        println("✗ Fallida: ${e.message}")\n    }\n}
\n\n

El punto clave: TestContext.() -> Unit es una lambda con receptor. Dentro del bloque, this hace referencia al objeto TestContext, por lo que los métodos should y shouldNot están disponibles implícitamente.

\n\n

Paso 2. Implementar los matchers

\n\n

Ahora crearemos matchers simples. beEqualTo y beNull son funciones que devuelven objetos Matcher:

\n\n
interface Matcher {\n    val expected: String\n    fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n    override val expected = "igual a $expected"\n    override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n    override val expected = "null"\n    override fun matches(value: Any?) = value == null\n}
\n\n

Blogs

Book Recommendations