Se você já usou Gradle com Kotlin, configurou rotas no Ktor ou montou layouts no Jetpack Compose, você já usou uma DSL sem perceber. DSL (Domain Specific Language) é um dos recursos mais elegantes de Kotlin, e neste post vamos entender como criar as nossas próprias.
O que é uma DSL?
Uma DSL é uma mini-linguagem projetada para um domínio específico. Diferente de uma linguagem de propósito geral (como Kotlin ou Java), uma DSL foca em resolver problemas de uma área particular de forma expressiva.
Exemplos de DSLs que você já conhece:
- SQL: linguagem para bancos de dados
- HTML: linguagem para páginas web
- Regex: linguagem para padrões de texto
Em Kotlin, podemos criar DSLs internas — ou seja, DSLs que são Kotlin válido, mas parecem uma linguagem própria.
A mágica por trás: lambdas com receiver
O ingrediente secreto das DSLs em Kotlin é a lambda com receiver. Vamos entender passo a passo:
// Função normal com lambda
fun executar(bloco: () -> Unit) {
bloco()
}
// Lambda COM receiver — dentro do bloco, `this` é do tipo StringBuilder
fun construirString(bloco: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.bloco() // o StringBuilder é o receiver
return sb.toString()
}
fun main() {
val resultado = construirString {
append("Olá, ") // `this` é StringBuilder, então append() está disponível
append("mundo!")
appendLine()
append("Kotlin DSL é demais!")
}
println(resultado)
}
O tipo StringBuilder.() -> Unit significa: “uma lambda onde o this é um StringBuilder”. Dentro dessa lambda, você pode chamar qualquer método de StringBuilder diretamente.
Criando uma DSL para HTML
Vamos construir uma DSL para gerar HTML — esse é o exemplo clássico:
class Tag(val nome: String) {
val filhos = mutableListOf<Any>()
val atributos = mutableMapOf<String, String>()
fun atributo(chave: String, valor: String) {
atributos[chave] = valor
}
operator fun String.unaryPlus() {
filhos.add(this)
}
fun tag(nome: String, bloco: Tag.() -> Unit = {}): Tag {
val novaTag = Tag(nome)
novaTag.bloco()
filhos.add(novaTag)
return novaTag
}
fun div(bloco: Tag.() -> Unit = {}) = tag("div", bloco)
fun h1(bloco: Tag.() -> Unit = {}) = tag("h1", bloco)
fun p(bloco: Tag.() -> Unit = {}) = tag("p", bloco)
fun a(href: String, bloco: Tag.() -> Unit = {}) = tag("a") {
atributo("href", href)
bloco()
}
override fun toString(): String {
val attrs = if (atributos.isEmpty()) "" else
atributos.entries.joinToString(" ", prefix = " ") { "${it.key}=\"${it.value}\"" }
val conteudo = filhos.joinToString("")
return "<$nome$attrs>$conteudo</$nome>"
}
}
fun html(bloco: Tag.() -> Unit): Tag {
val root = Tag("html")
root.bloco()
return root
}
fun main() {
val pagina = html {
tag("head") {
tag("title") { +"Kotlin Brasil" }
}
tag("body") {
h1 { +"Bem-vindo ao Kotlin Brasil!" }
p { +"Aprenda Kotlin em português." }
a("https://kotlin.dev.br") { +"Visite nosso site" }
}
}
println(pagina)
}
Repare no +"texto" — usamos operator overloading (unaryPlus) para adicionar texto como filho. O resultado é código que parece HTML, mas é Kotlin puro.
DSL para configuração
DSLs são perfeitas para configuração tipada e validada em compile time:
data class ServidorConfig(
var host: String = "localhost",
var porta: Int = 8080,
var ssl: SslConfig? = null,
var banco: BancoConfig? = null
)
data class SslConfig(
var certificado: String = "",
var chave: String = ""
)
data class BancoConfig(
var url: String = "",
var usuario: String = "",
var senha: String = "",
var poolSize: Int = 10
)
fun servidor(bloco: ServidorConfig.() -> Unit): ServidorConfig {
return ServidorConfig().apply(bloco)
}
fun ServidorConfig.ssl(bloco: SslConfig.() -> Unit) {
ssl = SslConfig().apply(bloco)
}
fun ServidorConfig.banco(bloco: BancoConfig.() -> Unit) {
banco = BancoConfig().apply(bloco)
}
fun main() {
val config = servidor {
host = "api.kotlin.dev.br"
porta = 443
ssl {
certificado = "/certs/server.crt"
chave = "/certs/server.key"
}
banco {
url = "jdbc:postgresql://localhost:5432/kotlinbrasil"
usuario = "admin"
senha = "s3nh4-s3gur4"
poolSize = 20
}
}
println("Servidor: ${config.host}:${config.porta}")
println("SSL: ${config.ssl != null}")
println("Banco pool: ${config.banco?.poolSize}")
}
Compare isso com um arquivo YAML ou properties — a DSL tem autocompletar, verificação de tipos e validação em tempo de compilação.
DSL para testes
Criar DSLs para testes deixa o código muito mais legível:
class PedidoTestBuilder {
var cliente: String = "Cliente Teste"
var itens: MutableList<ItemPedido> = mutableListOf()
var desconto: Double = 0.0
fun item(nome: String, preco: Double, quantidade: Int = 1) {
itens.add(ItemPedido(nome, preco, quantidade))
}
fun build() = Pedido(cliente, itens.toList(), desconto)
}
fun pedido(bloco: PedidoTestBuilder.() -> Unit): Pedido {
return PedidoTestBuilder().apply(bloco).build()
}
// Uso nos testes — lê como uma história
fun main() {
val meuPedido = pedido {
cliente = "Maria Silva"
desconto = 10.0
item("Camiseta Kotlin", preco = 79.90, quantidade = 2)
item("Caneca JVM", preco = 39.90)
item("Adesivo Coroutines", preco = 9.90, quantidade = 5)
}
println("Pedido de ${meuPedido.cliente}")
println("Total de itens: ${meuPedido.itens.size}")
}
@DslMarker: controlando o escopo
Em DSLs aninhadas, pode acontecer de acessar receivers de escopos externos acidentalmente. O @DslMarker previne isso:
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Body {
fun p(texto: String) { /* ... */ }
fun div(bloco: Div.() -> Unit) { /* ... */ }
}
@HtmlDsl
class Div {
fun span(texto: String) { /* ... */ }
// Não pode chamar p() do Body aqui — @DslMarker impede!
}
Isso torna a DSL mais segura e evita erros sutis de escopo.
DSLs famosas do ecossistema Kotlin
Você já usa DSLs Kotlin no dia a dia, talvez sem saber:
- Gradle Kotlin DSL:
build.gradle.ktsinteiro é uma DSL - Ktor: configuração de servidor e rotas
- Jetpack Compose: toda a UI é uma DSL
- Exposed: queries SQL type-safe
- Kotest: framework de testes com DSL expressiva
- Koin: injeção de dependência com DSL
Boas práticas
- Menos é mais: uma DSL boa é simples e focada
- Use
@DslMarker: em DSLs aninhadas, controle o escopo - Documente os builders: quem usa sua DSL precisa saber o que está disponível
- Valide no build: se algo obrigatório está faltando, falhe cedo com mensagem clara
- Teste a DSL: garanta que o código gerado está correto
Conclusão
DSLs são a cereja do bolo do Kotlin. Elas permitem criar APIs que lêem como documentação, são verificadas pelo compilador e proporcionam uma experiência de uso excepcional. Dominar lambdas com receiver e apply/with/run é a chave para desbloquear esse superpoder.
Agora é com você: pense num domínio do seu projeto que se beneficiaria de uma DSL e mãos à obra!