DSL em Kotlin: como criar sua própria linguagem de programação em uma noite

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

O que é DSL e por que você precisa dela?

\n\n

Imagine que você não está escrevendo código, mas sim uma instrução em linguagem clara que se transforma em um programa funcional. É exatamente assim que funcionam as linguagens específicas de domínio (DSL — Domain Specific Language). Diferente das linguagens de uso geral (Java, Python, C#), uma DSL é criada para resolver uma única tarefa específica — seja configuração de servidor, descrição de regras de negócio ou construção de consultas SQL complexas.

\n\n

Kotlin é o candidato ideal para criar DSLs. Graças às lambdas com receptor, extensões e type-safe builders, você pode escrever código que parece sintaxe nativa. Neste artigo, vamos abordar não apenas a teoria, mas criar uma DSL real para um framework de testes — e você verá como é simples e empolgante.

\n\n

Por que Kotlin é o rei das DSLs?

\n\n

Kotlin conquistou o amor dos desenvolvedores não apenas por sua compatibilidade com Java. Suas capacidades sintáticas permitem criar DSLs internas — linguagens que vivem dentro da linguagem hospedeira e usam sua sintaxe. Aqui estão os três pilares que sustentam qualquer DSL em Kotlin:

\n\n
    \n
  • Lambdas com receptor — permitem acessar métodos de um objeto sem especificar explicitamente seu nome. Esta é a base do padrão builder.
  • \n
  • Extensões — adicionam novas funções a qualquer classe, mesmo que você não possa modificá-la.
  • \n
  • Funções infixas — permitem escrever código no estilo de linguagem natural: should be equalTo 5.
  • \n
\n\n

Essas capacidades transformam a criação de DSLs em um conjunto de Lego: você pega blocos prontos — lambdas, funções, propriedades — e monta uma linguagem clara e segura.

\n\n

Criando uma DSL simples para testes: passo a passo

\n\n

Vamos escrever uma DSL para escrever testes. O objetivo é que os testes pareçam uma descrição legível, e não um conjunto de chamadas assert:

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

Concorde que isso é quase inglês. Agora vamos entender como funciona.

\n\n

Passo 1. Criando o contexto do teste

\n\n

Precisamos de uma função test que recebe um nome e um bloco de código. O bloco é executado em um contexto onde should e shouldNot estão disponíveis:

\n\n
class TestContext {\n    fun Any.should(matcher: Matcher) {\n        if (!matcher.matches(this)) {\n            throw AssertionError("Esperava ${matcher.expected}, mas obteve $this")\n        }\n    }\n\n    fun Any.shouldNot(matcher: Matcher) {\n        if (matcher.matches(this)) {\n            throw AssertionError("Não esperava ${matcher.expected}, mas obteve $this")\n        }\n    }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n    println("Executando teste: $name")\n    try {\n        TestContext().block()\n        println("✓ Aprovado")\n    } catch (e: AssertionError) {\n        println("✗ Reprovado: ${e.message}")\n    }\n}
\n\n

Ponto chave: TestContext.() -> Unit é uma lambda com receptor. Dentro do bloco, this se refere ao objeto TestContext, então os métodos should e shouldNot ficam implicitamente disponíveis.

\n\n

Passo 2. Implementando os matchers

\n\n

Agora vamos criar matchers simples. beEqualTo e beNull são funções que retornam 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 = "nulo"\n    override fun matches(value: Any?) = value == null\n}
\n\n

Blogs

Book Recommendations