Design Patterns em Scala – Parte 3: Pimp My Library

Nas duas primeiras partes da série Design Patterns em Scala, vimos alguns patterns bem conhecidos, porém implementados de uma maneira um pouco diferente em Scala.

Agora veremos um pattern que não é conhecido no mainstream Java, porque usa alguns recursos exclusivos da linguagem Scala, no caso de hoje esse recurso é a conversão implícita.

Estamos falando do Pimp My Library! Yooo!

Motivação

Se você leu o post sobre conversões implícitas, deve se lembrar do exemplo abaixo:

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

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

E agora, com toda a mágica desmistificada, você deve saber que está ocorrendo uma conversão implícita de String para alguma classe que contenha o método .toList().

Mas sem pensar no que está acontecendo por baixo dos panos, qual a sensação que temos quando escrevemos o código acima? Qual a percepção do desenvolvedor?

Parece que foram acrescentados métodos diretamente na classe final String, não é? Se observarmos o código, não dá nem para saber qual a classe que efetivamente define o método .toList().

Esse exemplo é exatamente uma aplicação do pattern Pimp My Library! A classe String foi… éé… “Pimpada”!

Ok, “incrementada”, “aumentada”, “melhorada”, são termos melhores… Mas enfim…

O Pimp My Library é útil para os casos em que você usa uma classe ou API que não é sua e deseja estendê-la, de maneira transparente, para melhor se adaptar às suas necessidades.

Esse pattern foi definido (e nomeado) pelo próprio Martin Odersky, criador da linguagem, nesse artigo.

A primeira vista esse pattern se parece muito com Ruby Open Classes ou até mesmo Prototypes em Javascript, no sentido de que se consegue atingir o mesmo efeito.

No entanto, fazer Pimp My Library em Scala é algo muito mais controlado e localizado. Você não está efetivamente fazendo uma alteração na classe e afetando todo restante da sua aplicação (o que pode sair do controle e introduzir bugs estranhos).

É mais controlado fazer isso em Scala porque você precisa trazer explicitamente para o escopo do seu código o método implicit que faz as coisas acontecerem.

Vamos entender melhor como funciona…

Implementação

Agora que sabemos como funcionam as conversões implícitas fica fácil fazer o resto!

Tudo que precisamos é fazer um rich wrapper que possui os métodos que desejamos acrescentar a classe a ser incrementada.

Você adoraria que String tivesse um método inverte() que te devolve a String de trás para frente? E mais, você quer esse método em português?? Sem problemas, faça você mesmo:

// A primeira coisa que você precisa é de um wrapper bem legal
class StringBombada(str:String) { 
  def inverte():String = {
    val listaRev = str.toCharArray.foldLeft(new ListBuffer[Char]) { (lista, char) => char +: lista }
    listaRev.mkString  
  }
}

// A segunda coisa é uma conversão implicita no escopo. 
// Você pode deixar esse método dentro de um object.
implicit def stringParaStringBombada(str:String):StringBombada = {
  new StringBombada(str)
}

E a prova final:

scala> "socorram-me subi no onibus em marrocos".inverte
res17: String = socorram me subino on ibus em-marrocos

E é só isso. Lembrando que a conversão só acontecerá quando o implicit estiver no escopo.

Quick Pimp

Se você quer fazer uma coisa mais rápida, adicionar só um método aqui e ali, e acha que escrever um Wrapper é muito trabalhoso, apresento-lhe o Quick Pimp.

O Quick Pimp é uma versão light do Pimp My Library, que usa Structured Typing e Classes Anônimas ao invés de um Wrapper completo e dedicado:

implicit def string2paraInteiro(s: String): { def paraInteiro: Int } = new {
  def paraInteiro: Int = java.lang.Integer.parseInt(s)
 }
}

Veja, só com esse implicit no escopo eu consigo fazer isso:

scala> "12345" paraInteiro
res1:Int = 12345

Prático, conciso, elegante…

O QuickPimp se baseia em Structural Typing que internamente depende de reflection. Portanto, pode ter algum impacto em partes em que a performance é crítica.

Recapitulando

Nesse post começamos a entrar mais a fundo no mundo exclusivo de Scala, conhecendo um pattern muito usado, inclusive na própria API do Scala, chamado Pimp My Library, e sua variação light Quick Pimp.

Esse pattern lembra os open-classes e monkey patching do Ruby, mas de uma forma mais controlada e type-safe. É extremamente útil para incrementar classes e APIs existentes, mesmo que elas sejam de terceiros, e mesmo que elas sejam final.

E por hoje é só!

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…)