Implicits em Scala – Conversões implícitas e dinamites


Explícito é o que você está acostumado a ver na TV de madrugada, implícito é outra coisa. Veja:

implícito
adj.
1. Incluído, contido (ainda que não expressado), subentendido.

Isso mesmo! Conversões implícitas são conversões subentendidas, que acontecem por baixo dos panos, que o compilador faz sem você perceber.

E para que serve isso? É esse o segredo da mágica do Scala! É o pó mágico que nos permite fazer DSLs que são ao mesmo tempo poderosíssimas e type-safe!

Já se imaginou fazendo isso:

scala> val str = "Hello"
str: java.lang.String = Hello

scala> str.toList
res0: List[Char] = List(H, e, l, l, o)

Eu olhei a API de java.lang.String e não tem nenhum método toList! E String é final!

E que tal definir mapas assim?

scala> val mapa = Map(1 -> "Um", 2 -> "Dois")
mapa: scala.collection.immutable.Map[Int,java.lang.String] = Map((1,um), (2,dois))

Não, nada disso é nativo da linguagem. São apenas conversões implícitas de String para alguma classe que contenha o método toList, e de Int para alguma classe que contenha o método ->.

Implicit Methods

A mágica aí em cima é feita por métodos implícitos que estão definidos no objeto scala.Predef que é importado automaticamente pelo compilador.

Mas como esses métodos implícitos funcionam?

O conceito é bastante simples na verdade. Um método implícito é um método que recebe um objeto de alguma classe e devolve um objeto de outra classe, e também é explicitamente marcado como implícito =) .

Vamos fazer alguns exemplos. Primeiro vamos definir o seguinte método:

def imprime(str:String) = println(str)

Um método bobo que recebe uma String e a imprime no console. Vai lá, faça um teste no console do Scala:

scala> imprime("Uma String")
Uma String

Agora tente passar um Int para nosso método:

scala> imprime(1234)
found : Int(1234)
required: String

Holy Shit! Erro de compilação! 1234 não é uma String.

Agora defina o seguinte método implícito de conversão de Int para String.

// Não faça isso de verdade, ok?
implicit def intToString(inteiro:Int):String = {
  println("Implicit em ação!") // Isso é para provar que essa função é chamada
  inteiro.toString
}

Agora vamos tentar chamar nosso método mais uma vez com um inteiro:

imprime(1234)
Implicit em ação!
1234

Aha! Como esperado! Nosso implicit foi chamado e o parâmetro foi convertido.

O compilador muito inteligentemente fez o seguinte:

  • viu que o método imprime recebe uma String como parâmetro, mas o programador passou um Int no lugar.
  • então ele procurou no escopo da chamada, algum método marcado com implicit que recebe um Int e devolve uma String. No nosso caso o método intToString.
  • colocou uma chamada para intToString(1234) substituindo o parâmetro 1234.

Resumindo, ele transformou isso:
imprime(1234)
Nisso:
imprime(intToString(1234))

Se você não estivesse tão maravilhado com esse fantástico exemplo, talvez estivesse pensando:
– Essa ***** não explica os exemplos do começo do Post!

Muito perspicaz você hein!

O fato é que além de funcionar no momento de passagem de parâmetros, as conversões implícitas também entram em ação em chamadas de método, ou seja, se você tentar chamar um método que não existe em uma classe XYZ, o compilador vai procurar por um método implícito no escopo que saiba converter a classe XYZ para alguma classe que contenha o método chamado.

Por exemplo:

class DateHelper(val qtd:Int) {
  def segundos = qtd * 1000L
  def minutos = 60 * segundos
  def horas = 60 * minutos
  def dias = 24 * horas
}

Esse wrapper é construido passando-se um inteiro para ele. Quando chamamos qualquer um de seus métodos, ele devolve uma quantidade de milisegundos se for segundos, minutos, horas ou dias.

Normalmente isso não seria algo muito prático de se usar:

val dh = new DateHelper(2);
dh.minutos // 120000

Mas se tivermos um implicit de Int para DateHelper, as coisas ficam muito mais interessantes:

implicit def intToDateHelper(qtd:Int) = new DateHelper(qtd)

E portanto teremos algo muito fantástico:

scala>2 minutos
120000
scala>5 dias
432000000

Praticamente estamos escrevendo em português aqui =)

E é assim que podemos fazer poderosas DSLs em Scala!

Implicit Parameters

Em Scala existe um outro tipo de implict. Os parâmetros implícitos.

Quando um parâmetro marcado como implicit não é fornecido pelo programador, o compilador vai procurar no escopo da chamada alguma variável também marcada como implicit que possa ser usada como parâmetro.

Imagine a função:

implicit val numero = 10

def soma(a:Int)(implicit b:Int) = a + b

soma(2)(2) == 4  // Se não precisar, o implicit não é usado
soma(2) == 12 // usa implicit
soma(1) == 11 // usa implicit

Uma das aplicações disso é o uso de Manifests para obter informações de tipo em runtime. (um dia escrevo um post sobre isso).

Algumas regras

O compilador segue algumas regrinhas para determinar qual implicit usar e quando usar.

Basicamente:

  • Somente é considerado o que estiver marcado como implicit
  • Somente é considerado os implicits que estiverem no escopo.
  • Se o compilador encontrar mais de um implicit que possa ser aplicado (ambiguidade), dá erro.
  • Somente UMA conversão implícita é tentada pelo compilador. Por exemplo, se um método espera C, você passa A, e existe uma conversão A->B e outra B->C. O compilador NÃO faz.
  • O compilador só faz uma conversão se for necessário. Se o que estiver escrito fizer sentido (type-check), o compilador não aplica nenhuma conversão.

E as dinamites?

O que diabos dinamites tem a ver com isso?

Quem já assistiu a série Lost deve se lembrar do finado Dr. Artz que morreu explodido por uma dinamite.

O que eu quero dizer aqui é simples: Implicits são extremamente poderosos, mas tome muito cuidado ao usá-los. Não saia por aí fazendo conversões implícitas sem cautela porque você vai:

  • Criar um código muito difícil de entender.
  • Introduzir os bugs mais bizarros que você já viu no seu código.
  • Morrer de caganeira.

Com grande poder vem grande responsabilidade.

Pense nisso. (ou se exploda, você quem sabe…)

Anúncios