Programação, Scala

Conversão implícita de tipos em Scala

Scala é uma linguagem fortemente tipada: ao utilizarmos uma função que leva uma String como parâmetro, passando-lhe um Int, deve haver uma conversão implicita qualquer para que não ocorra um erro de compilação. Por exemplo, em println(10) chamamos a função println com um inteiro cujo valor literal é 10. Sabemos que 10 é um objeto, logo uma função deste objeto é chamada para convertê-lo a uma String, a qual é impressa por println. Tudo isso acontece por trás das cortinas, sem a percepção do programador.

Como acontece a conversão de Int em String neste exemplo? A resposta é : através da conversão implícita de tipos.

Conversores implicitos(palavra-chave implicit)
Scala oferece a funcionalidade de “conversores e parâmetros implícitos”. Conforme o nome sugere, são funções que convertem tipos para permitir que o comportamento intuitivo dos tipos aconteça sem conversões explícitas no código.

val dez = 10 // Um Int qualquer
println(dez) // println recebe uma String, mas dez é um Int

Como o Int de valor 10 foi impresso sem gerar um erro de compilação, se sabemos que println recebe uma String como parâmetro?

A biblioteca padrão da linguagem Scala possui um módulo chamado Preamble onde existem diversas rotinas de conversão implícita de tipos. Há uma função específica para a transformação de Int em String, assim toda vez que o compilador encontra uma função cuja assinatura possui um parâmetro marcado como implicit, a qual é chamada com um tipo incompatível, este procurará no escopo atual uma rotina de conversão implicita para evitar a falha.

Vejamos um exemplo mais completo.
case class Euro (qto: Int) {
implicit def toDollar(e: Euro) : Dollar = Dollar(2 * qto)
}
case class Dollar(qto: Int)
object implicitos {
implicit def toEuro(d: Dollar) : Euro = Euro((.5 * d.qto).toInt)
implicit val ex : String = "Os mafagafinhos foram desmafagafados?\n"
def countEuros(eur: Euro) = {
eur.qto
}
def imprimeAlgo(implicit x: String) = println(x)
def main(args: Array[String]) {
val d: Dollar = Dollar(15)
println(countEuros(d))
imprimeAlgo
}
}

Primeiro definimos duas case classes, uma representando o Dólar e a outra Euros. Dollar e Euros são dois tipos distintos, portanto uma função que receba um parâmetro em Euro não poderá usar um objeto Dollar no seu lugar. Isso, se não tivessemos conversores implicitos!

Dentro do objeto implicitos, definimos uma função implicit def toEuro, a qual recebe um parâmetro em Dollar e o devolve convertido em Euro. Logo adiante instanciamos uma quantia de 15 Dolares, e imediatamente chamamos a função countEuros que deve nos dizer quantos Euros esse objeto representa. Note que a assinatura de countEuros recebe uma quantia em Euro, não em Dolares. O compilador detectou o conflito de tipos e procurou, no mesmo escopo, uma função de conversão implicita de Dolares -> Euros. O valor convertido foi então passado corretamente à função. Desta forma, o sistema Scala fez “câmbio” de moedas para nós e o código funcionou de forma intuitiva, afinal é perfeitamente normal calcular a quantos Euros equivale uma certa quantia de Dolares.

Por fim, um outro exemplo do uso de implicit: a rotina imprimeAlgo é chamada sem parâmetros, mas sabe-se que esta rotina exige um parâmetro do tipo String. No caso, o compilador buscou no escopo da chamada um valor String implicito que fosse capaz de preencher o parâmetro omitido. Esta funcionalidade é muito utilizada nas bibliotecas padrão de Scala, onde as rotinas usam parâmetros implicitos que podem ser determinados a partir do ambiente local.

Para usar esta funcionalidade, a função que converte do tipo Dolar para Euro deve ser implícita, ou o compilador não tentará substituir o valor. O motivo desta exigência é evitar efeitos colaterais inesperados: o compilador não escolhe simplesmente qualquer função compatível e a utiliza, as funções devem ser marcadas como implicit para torná-las candidatas, assim o desenvolvedor controla qual comportamento será permitido.

Conclusão
Conversões implícitas de tipos permitem código mais limpo e legível, evitando descrições redundantes de conversões que devem ocorrer naturalmente. Podemos determinar exatamente quais rotinas de conversão usar em cada escopo. O compilador converterá apenas através de rotinas marcadas como implicit, de forma a garantir um comportamento pré-determinado. É importante compreender a semântica dos parâmetros implicitos, pois o idioma é frequentemente utilizado nas bibliotecas padrão Scala e frameworks como o Play Framework.

Standard