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.

Anúncios