Extraindo valores com unapply (extractor methods)

Quem já teve contato com Scala provavelmente já ouviu falar do famigerado Pattern Matching (vulgo “switch-case” bombadão).

E realmente esse negócio parece mágico, consegue fazer match de tudo quanto é coisa: String, List, case classes etc etc. Na realidade, vamos ver mais pra frente que ele realmente pode fazer match de qualquer coisa.

Olha esse exemplo que fantástico:

case class Pessoa(nome:String, idade:Int)
case class Cachorro(nome:String, dono:Pessoa)

val dono = Pessoa("Homer", 43)
val pet = Cachorro("Ajudante de papai noel", dono)

pet match {
  case Cachorro( nome, Pessoa("Homer",_) ) => println(nome + " é o cachorro do Homer") // Match de todos os 'Cachorro' cujo 'dono' se chama "Homer"
  case Cachorro( nome, Pessoa("Bart", _) ) => println(nome + " é o cachorro do Bart") // Match de todos os 'Cachorro' cujo 'dono' se chama "Bart"
}

>> Ajudante de papai noel é o cachorro do Homer

Absurdo! Consigo até fazer match com uma case class dentro da outra! (momento paga pau).

Ok, parece que eu estou fugindo do assunto do título, mas na verdade case classes tem tudo a ver com esse tal de unapply.

Para entendermos mais a fundo o funcionamento de um match, vamos definir o seguinte singleton:

object MeuExtrator {
  def unapply(s:String):Option[String] = Some(s.head)
}

Esse objeto define um método extractor bem besta, que faz match em uma String e extrai a primeira letra dessa String, mas já podemos começar entender o que o Scala faz com esse unapply.

O método unapply deve receber como parâmetro o elemento no qual se deseja fazer o match e deve devolver uma dessas três coisas:

  • subclasse de Option[T] : caso se deseja extrair do elemento um valor T qualquer.
  • subclasse de Option([T1, … , Tn)]: caso se deseja extrair do elemento N valores diferentes.
  • Boolean: nesse caso o unapply funciona apenas como uma verificação (por exemplo, um extrator chamado ehpar(i:Int):Boolean, que não extrai nada, mas indica se o elemento é par.

Agora vamos usar nosso extrator:

"Bisnaga" match {
  case MeuExtrator(letra) => println(letra)
}

Eis o que o compilador faz quando vê esse “case MeuExtrator(letra)”:

  1. Executa o método MeuExtrator.unapply() passando como parâmetro a String “Bisnaga”.
  2. Ele vê que o unapply devolve um Some[_] e coloca o conteúdo desse Some na variável letra.
  3. Como o match foi feito, executa o que tiver depois da setinha =>

Hum, agora está começando a fazer sentido. Posso fazer também um extractor que extrai algumas informações de uma instância de Produto.

class Produto(var nome:String, var preco:Int, var isDisponivel:Boolean) {
  // Produto com alguns campos.
}

object Produto {
  def unapply(p:Produto):Option[(String, Int, Boolean)] = {
    Some( (p.nome, p.preco, p.isDisponivel) )
  }
}

val umProduto = new Produto("IPhone", 2000, true )

umProduto match {
  case Produto(nome, valor, estaDisponivel) =>
    if(estaDisponivel) {
      println(nome + " disponivel por R$" + valor)
    } else {
      println("Nao tem nenhum " + nome + " hoje.")
    }
  case _ => println("Nenhum produto encontrado.")
}

Massa! Mas o que case classes tem a ver com extractors?

Quando você cria uma case class o compilador cria para você (entre outras coisitas) um método unapply que extrai cada um dos campos definidos para a case class.

Pronto, sabemos construir extractors! Agora você pode sair por aí fazendo Pattern Matching de tudo pela frente.

Queries case insensitive com Like no Mapper

Hoje eu estava implementando buscas para uma aplicação em Lift 2.0 e queria fazer uma Query com Like.
No Mapper isso pode ser feito da seguinte maneira:

Usuario.findAll(Like(Usuario.nome, "FE%"))

Simples e expressivo não? Essa busca deveria me trazer os usuários cujo nome começa com “Fe”, como Felipe, Fernando, Felícia…
Só que ela não me trouxe nada! Porque eu digitei FE com letra maiúscula na tela… humm… com certeza deve existir um LikeNoCase no Mapper!
Não tem… Mas descobri que o Like na verdade é um builder para a criação de um objeto mais genérico chamado Cmp, que representa uma comparação na minha Query.
Eis a definição da classe Cmp:

final case class Cmp[O<:Mapper[O], T](field: MappedField[T,O], opr: OprEnum.Value, value:Box[T], otherField: Box[MappedField[T, O]], dbFunc: Box[String]) extends QueryParam[O]

A solução então é fazer o seguinte

Usuario.findAll(Cmp(Usuario.nome, OprEnum.Like, Full("FE%"), Empty, Full("UPPER"))

O último argumento informa qual função SQL quero aplicar na coluna que estou comparando. Dessa forma, consigo uma comparação Like Case Insensitive.
O penúltimo parâmetro do construtor do Cmp não tenho idéia do que seja, não tem nada no Scaladoc, que por sinal poderia melhorar muito!

Então é isso, fica aí a dica rápida.

def first = “Hello world!”

Atualmente trabalho com Java e tenho que dizer que estou ficando cansado de tanta verbosidade. Um dia desse ouvi falar de um tal de Scala… Curioso que sou, fui ver qual é que é.

Bom, tenho que dizer que Scala é massa pra cacete. E alguns frameworks construidos sobre essa linguagem, como Lift, Akka e Squeryl me deixaram bem impressionados.

Não é uma linguagem fácil, o que torna as coisas mais interessantes. Mas também não é difícil.

Como tenho memória de peixe, fiz esse blog para escrever algumas coisas que fui (e ainda vou) descobrindo. Como o Google é a fonte primária de respostas para muitas pessoas, talvez esse blog venha a ajudar alguém um dia.

Vai saber.