O que e DSL em Kotlin?
DSL significa Domain-Specific Language – uma linguagem específica de dominio. Em Kotlin, você pode criar DSLs usando lambdas com receiver e extension functions, resultando em código que parece quase uma linguagem natural para determinada tarefa.
Se você já usou Gradle com Kotlin Script ou escreveu testes com Kotest, já viu DSLs em acao.
Pense numa DSL como um formulario bem desenhado: em vez de escrever texto livre, você preenche campos estruturados que guiam a resposta. A DSL faz isso com código – cria uma estrutura para expressar intencoes de forma clara e restrita ao dominio.
O segredo por tras das DSLs em Kotlin esta na combinacao de lambdas com receiver, higher-order functions e extension functions. Esses tres recursos juntos permitem criar APIs que parecem uma linguagem própria.
Exemplo simples: construtor de HTML
class HTML {
private val elementos = mutableListOf<String>()
fun head(titulo: String) {
elementos.add("<head><title>$titulo</title></head>")
}
fun body(conteudo: Body.() -> Unit) {
val body = Body()
body.conteudo()
elementos.add("<body>${body.render()}</body>")
}
fun render() = "<html>${elementos.joinToString("")}</html>"
}
class Body {
private val itens = mutableListOf<String>()
fun p(texto: String) { itens.add("<p>$texto</p>") }
fun h1(texto: String) { itens.add("<h1>$texto</h1>") }
fun render() = itens.joinToString("")
}
fun html(bloco: HTML.() -> Unit): String {
val pagina = HTML()
pagina.bloco()
return pagina.render()
}
E o uso fica assim:
fun main() {
val pagina = html {
head("Kotlin Brasil")
body {
h1("Bem-vindo!")
p("Aprenda Kotlin de um jeito brasileiro.")
}
}
println(pagina)
}
Repare como o código fica expressivo – quase como escrever HTML de verdade.
A magica: lambdas com receiver
O segredo das DSLs em Kotlin e a lambda com receiver. Quando você declara HTML.() -> Unit, esta dizendo que dentro do bloco, o this e uma instancia de HTML. Isso permite chamar os métodos diretamente, sem prefixo.
Exemplo prático: configuração
class DatabaseConfig {
var host = "localhost"
var porta = 5432
var nome = ""
}
fun database(config: DatabaseConfig.() -> Unit): DatabaseConfig {
return DatabaseConfig().apply(config)
}
fun main() {
val db = database {
host = "db.kotlin.dev.br"
porta = 5433
nome = "kotlin_brasil"
}
println("${db.host}:${db.porta}/${db.nome}")
}
Exemplo: DSL para definicao de rotas HTTP
Um caso muito comum em frameworks web e a definicao de rotas usando DSL. Veja como ficaria uma versão simplificada:
class Router {
private val rotas = mutableListOf<String>()
fun get(caminho: String, handler: () -> String) {
rotas.add("GET $caminho -> ${handler()}")
}
fun post(caminho: String, handler: () -> String) {
rotas.add("POST $caminho -> ${handler()}")
}
fun listar() = rotas.forEach(::println)
}
fun router(config: Router.() -> Unit): Router {
return Router().apply(config)
}
fun main() {
val api = router {
get("/usuarios") { "Lista de usuarios" }
get("/usuarios/{id}") { "Detalhes do usuario" }
post("/usuarios") { "Criar usuario" }
}
api.listar()
}
Exemplo: DSL para construcao de queries
class Query {
private var tabela = ""
private val condicoes = mutableListOf<String>()
private var limite: Int? = null
fun from(nome: String) { tabela = nome }
fun where(condicao: String) { condicoes.add(condicao) }
fun limit(n: Int) { limite = n }
fun build(): String {
val sql = StringBuilder("SELECT * FROM $tabela")
if (condicoes.isNotEmpty()) {
sql.append(" WHERE ${condicoes.joinToString(" AND ")}")
}
limite?.let { sql.append(" LIMIT $it") }
return sql.toString()
}
}
fun query(config: Query.() -> Unit): String {
return Query().apply(config).build()
}
fun main() {
val sql = query {
from("pedidos")
where("status = 'ativo'")
where("valor > 100")
limit(10)
}
println(sql)
// SELECT * FROM pedidos WHERE status = 'ativo' AND valor > 100 LIMIT 10
}
Controlando o escopo com @DslMarker
Quando você tem DSLs aninhadas, pode acontecer de o usuário acessar funções do escopo externo dentro do escopo interno por engano. A anotacao @DslMarker resolve isso:
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Table {
fun tr(block: Row.() -> Unit) { /* ... */ }
}
@HtmlDsl
class Row {
fun td(texto: String) { /* ... */ }
}
Com @DslMarker, dentro de um bloco tr { } você só tem acesso aos métodos de Row, não aos de Table. Isso evita erros e deixa a DSL mais segura.
Casos de Uso no Mundo Real
- Gradle Kotlin DSL: toda a configuração de build do Gradle pode ser feita com Kotlin DSL, incluindo dependências, plugins e tarefas customizadas.
- Jetpack Compose: a UI declarativa do Android usa DSLs extensivamente. Cada Composable e essencialmente uma DSL para construir interfaces.
- Ktor: o framework web da JetBrains usa DSL para definir rotas, configurar servidores e serializar dados.
- Kotest: framework de testes que usa DSL para descrever testes de forma expressiva, similar ao RSpec do Ruby.
- Exposed: framework de banco de dados da JetBrains que usa DSL type-safe para construir queries SQL.
Boas Praticas
- Mantenha a DSL focada em um único dominio. Uma DSL que tenta fazer tudo vira uma GPL mal feita.
- Use
@DslMarkerpara controlar o escopo e evitar que o usuário acesse funções de contextos externos indevidamente. - Documente a DSL com exemplos claros, pois a sintaxe pode não ser obvia para quem não conhece a API.
- Prefira nomes que leiam como linguagem natural.
get("/rota")e melhor do queadicionarRotaGet("/rota").
Erros Comuns
- Nao usar @DslMarker: sem essa anotacao, o usuário pode chamar funções do escopo externo dentro de blocos aninhados, causando comportamento inesperado.
- DSL complexa demais: se o usuário precisa ler a documentação inteira pra entender como usar, a DSL esta complicada demais. Simplifique.
- Confundir DSL com builder pattern: embora DSLs possam usar builders, nem todo builder e uma DSL. A DSL deve ter uma sintaxe que faça sentido no dominio.
- Esquecer de validar entradas: como a DSL permite ao usuário preencher campos livremente, e essencial validar os dados no momento da construcao do objeto final.
Perguntas Frequentes
Qual a diferenca entre DSL e API fluente? Uma API fluente usa encadeamento de métodos (method chaining), enquanto uma DSL usa lambdas com receiver para criar blocos de código que leem como uma linguagem própria. Em Kotlin, DSLs sao mais poderosas porque permitem aninhamento natural.
Preciso saber generics para criar DSLs? Nao necessariamente para DSLs simples, mas generics ajudam muito quando você quer criar DSLs reutilizaveis e type-safe. Muitas DSLs avancadas combinam generics com inline functions.
DSLs afetam a performance? Minimamente. Como as lambdas com receiver sao compiladas para classes anonimas na JVM, há uma pequena alocacao de memória. Usando funções inline, esse custo e eliminado, pois o compilador insere o código diretamente no local da chamada.
Posso criar DSLs que funcionem em Kotlin Multiplatform? Sim. Como DSLs sao construidas com recursos da própria linguagem Kotlin (lambdas, extension functions, receivers), elas funcionam em todas as plataformas suportadas pelo Kotlin, incluindo JVM, JS e Native.
DSLs em Kotlin sao poderosas para configuracoes, builders de UI, definicao de rotas em servidores web e muito mais. E uma das features que mais diferencia Kotlin de outras linguagens.