Parâmetros de contexto em Kotlin
Publicado em Developer
kotlin

Faz alguns dias (23/06) foi liberada a versão 2.2.0 da linguagem Kotin, com algumas novidades, entre elas o suporte beta aos parâmetros de contexto. Vejam que ainda é beta, está em preview, pode mudar, mas já saiu da fase anterior (de onde inclusive já foram feitas algumas alterações), mas aparentemente vai continuar nesse rumo mesmo, mas por enquanto, para compilar o código que será visto abaixo, é necessário os parâmetros -Xcontext-parameters, ok?
Que diabos são os parâmetros de contexto? Antes de explicar o conceito envolvido, vamos dar uma olhada em injeção de dependências. Vamos imaginar que temos uma classe Person, onde para cada método executado, registra em um log. O log pode ser exibido na tela ou gravado em um arquivo. Para efeitos didáticos, vamos exibir ambos na tela, apenas com mensagens diferentes. Se tivéssemos esse código:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Person(val name: String, val logger: Logger) {
fun hello() {
logger.log("Hello, $name!")
}
fun bye() {
logger.log("Bye!")
}
}
fun main() {
val console = ConsoleLogger()
val file = FileLogger()
val person = Person("tag", console)
person.hello()
person.bye()
}
Ali definimos uma interface chamada Logger
, e derivamos nossos Loggers dela. Temos o ConsoleLogger
, que vai mostrar a mensagem no terminal, e o FileLogger
, que iria gravar a mensagem em um arquivo, mas conforme já mencionado, para efeitos didáticos irá mostrar a mensagem de uma forma diferente no terminal. Quando iniciamos Person
, enviamos o Logger
que desejamos (essa é a injeção por construtor), de forma com que se quiséssemos enviar outro Logger
, seria só trocar ali no construtor, já que ambos implementam a mesma interface. Isso nos permite, dentro dos métodos de Person
, acionar o método log do Logger
enviado:
Log: Hello, taq! Log: Bye!
Isso funciona perfeitamente, mas vamos imaginar uma situação que desejamos que Person
seja acionada primeiro com o ConsoleLogger
e depois com o FileLogger
. Se criamos cada Person
com injeção direto no construtor, teríamos que criar outra Person
, igual a anterior, com o outro Logger
, o que seria bem redundante. Uma alternativa seria ajustar o Logger
de acordo com o uso:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Person(val name: String) {
lateinit var logger: Logger
fun hello() {
logger.log("Hello, $name!")
}
fun bye() {
logger.log("Bye!")
}
}
fun main() {
val person = Person("tag")
person.logger = ConsoleLogger()
person.hello()
person.bye()
person.logger = FileLogger()
person.hello()
person.bye()
}
Log: Hello, taq! Log: Bye! Log to file: Hello, taq! Log to file: Bye!
Esse é um dos contextos que os parâmetros de ... contexto, ajudam. Do jeito que está, agora a classe Person
usa lateinit var logger: Logger
em vez de receber o logger no construtor, e na função main()
podemos ver como o logger é atribuído dinamicamente e testado com diferentes implementações (ConsoleLogger
e FileLogger
). A diferença principal é que agora o logger pode ser trocado em tempo de execução, demonstrando melhor a flexibilidade do padrão Strategy/Dependency Injection. Mas também podemos fazer algo do tipo:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Person(val name: String) {
context(logger: Logger)
fun hello() {
logger.log("Hello, $name!")
}
context(logger: Logger)
fun bye() {
logger.log("Bye!")
}
}
fun main() {
val person = Person("tag")
with(ConsoleLogger()) {
person.hello()
person.bye()
}
with(FileLogger()) {
person.hello()
person.bye()
}
}
Podemos ver que aqui não enviamos o Logger
em nenhum dos momentos anteriores, seja no construtor, seja atribuindo ele mais tarde, mas sim através de with
com o objeto correspondente dentro. Também em Person
, temos que indicar nos métodos que eles serão executados dentro de um determinado contexto, utilizando context
(dãr!), sendo necessário indicar uma referência e o tipo do objeto.
A diferença principal é que agora:
- As funções
hello()
ebye()
têmcontext(logger: Logger)
em suas assinaturas - Na função
main()
, usamoswith(Logger())
para criar o contexto onde as funções podem acessar o logger implicitamente - Não precisamos mais de
lateinit var
nem passar o logger explicitamente
Em versões anteriores da linguagem aparentemente não era necessário a referência, sendo inferido o objeto de forma automática, mas foi alterado, até pelo fato de quando eram enviados objetos que tinham o mesmo método, era necessário identificar cada um, o que acaba ficando melhor e mais claro com uma referência para cada um. Também pode ser utilizada na referência um parâmetro anônimo com "_", mas acaba também ficando mais verboso.
Feito isso, nos métodos de Person
que esperam um contexto, podemos abrir o context instanciando o objeto desejado correspondente (seja ConsoleLogger
ou FileLogger
, etc) e executar os métodos de Person
dentro do contexto enviado. Inclusive, o contexto faz propagação automática. Vejamos no caso de hello()
chamando bye()
, que resulta no mesmo output visto acima:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Person(val name: String) {
context(logger: Logger)
fun hello() {
logger.log("Hello, $name!")
bye()
}
context(logger: Logger)
fun bye() {
logger.log("Bye!")
}
}
fun main() {
val person = Person("tag")
with(ConsoleLogger()) {
person.hello()
person.bye()
}
with(FileLogger()) {
person.hello()
person.bye()
}
}
Podemos ver uma característica interessante dos context receivers: na função hello()
, ela chama bye()
diretamente, sem precisar passar o logger explicitamente. O contexto é automaticamente propagado entre as funções que têm o mesmo context receiver. Isso demonstra como os context receivers facilitam a composição de funções que dependem do mesmo contexto, tornando o código mais limpo e natural.
E se precisarmos de vários contextos enviados? Não tem problema, podemos utilizar vários blocos de with
:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Database {
context(logger: Logger)
fun save(person: Person) {
logger.log("Saving ${person.name}")
}
context(logger: Logger)
fun commit() = logger.log("Committing!")
}
class Person(val name: String) {
context(logger: Logger)
fun hello() {
logger.log("Hello, $name!")
}
context(logger: Logger)
fun bye() {
logger.log("Bye!")
}
}
fun main() {
val console = ConsoleLogger()
val database = Database()
val person = Person("tag")
with(console) {
with(database) {
person.hello()
database.save(person)
person.bye()
database.commit()
}
}
}
Aqui vemos um exemplo mais complexo com múltiplos context receivers aninhados. A classe Database
também foi adicionada, demonstrando como diferentes classes podem compartilhar o mesmo contexto de Logger
. Na função main()
, vemos o uso de with
aninhados onde o contexto do console (Logger
) é compartilhado entre todas as operações, tanto da Person
quanto da Database
. Isso mostra a flexibilidade e elegância dos context receivers para gerenciar dependências de forma implícita.
E esse caso onde Person
recebe os contextos de Logger
e Database
, executando save()
dentro de hello()
e commit()
dentro de bye()
:
interface Logger {
abstract fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
class FileLogger : Logger {
override fun log(msg: String) {
println("Log to file: $msg")
}
}
class Database {
context(logger: Logger)
fun save(person: Person) {
logger.log("Saving ${person.name}")
}
context(logger: Logger)
fun commit() = logger.log("Committing!")
}
class Person(val name: String) {
context(logger: Logger, database: Database)
fun hello() {
logger.log("Hello $name!")
database.save(this)
}
context(logger: Logger, database: Database)
fun bye() {
logger.log("Bye!")
database.commit()
}
}
fun main() {
val console = ConsoleLogger()
val database = Database()
val person = Person("tag")
with(console) {
with(database) {
person.hello()
database.save(person)
person.bye()
database.commit()
}
}
}
Esta versão mostra um dos recursos mais poderosos dos context receivers: múltiplos contextos na mesma função. Na classe Person
, as funções hello()
e bye()
agora têm context(logger: Logger, database: Database)
, permitindo acesso implícito a ambos os objetos. Isso elimina a necessidade de passar essas dependências como parâmetros ou armazená-las como propriedades da classe.
A função hello()
chama database.save(this)
e a função bye()
chama database.commit()
, demonstrando como múltiplas dependências podem ser gerenciadas de forma elegante através dos context receivers.
Foi uma pinceladinha rápida sobre esse recurso novo, que ainda pode mudar, mas já dá para ir imaginando vários usos dele.
Happy Hacking! :-)
0 comentário - Comente esse artigo!
Artigos anteriores
- Guia Completo do vim-rails - sáb, 12 de julho de 2025, 14:12:53 -0300
- Pull requests em modo raiz - sex, 22 de dezembro de 2023, 09:57:09 -0300
- Qual a idade do seu repositório? - ter, 27 de dezembro de 2022, 12:50:35 -0300
- Utilizando ctags em projetos Rails mais recentes - qui, 24 de junho de 2021, 08:23:43 -0300
- Fazendo o seu projeto brotar - seg, 15 de julho de 2019, 08:57:05 -0300
- Learn Functional Programming with Elixir - sex, 02 de março de 2018, 18:47:13 -0300
- Ambiente mínimo - Driver Driven Development - qua, 23 de agosto de 2017, 15:15:03 -0300
- Ambiente mínimo - repositórios de código - dom, 16 de abril de 2017, 13:02:14 -0300
- Ambiente mínimo - terminal e navegador - dom, 02 de abril de 2017, 21:43:29 -0300