«

»

Mar 14

Scala: Tipos parametrizados, limites superiores, inferiores e métodos polimórficos

Este é o sexto post da série sobre Scala, o primeiro foi o básico da linguagem scala, logo depois Orientação a objetos em scala, um pouco sobre closures, depois compreensão de listas e mapas, e na semana passada sobre Programação Funcional em Scala. Outra coisa bastante comum em todo código Scala que vejo por ai são Type Parameters, que em Scala podem assumir mais de uma forma, e também precisamos entender como funcionam upper e lower bounds, ou limites superiores e inferiores.

Vamos começar com alguns exemplos, e vamos analisar os códigos para tentar deixar claro como isto funciona e como podemos utilizar este recurso bastante poderoso a nosso favor.

(este código não é executável, é apenas um exemplo)
samples/022_typeparamsbasic.scala

1
2
3
4
5
6
7
8
9
//Declaração parcial da classe Array em Scala
class Array[A] {
def apply(index: Int): A
def update(index: Int, elem: A)
}
 
val x = new Array[String]()
val y: Array[Any] = x
y(0) = new Rational(1, 2) // Açucar sintatico para y.update(0, new Rational(1, 2))

Neste exemplo, podemos ver um exemplo simples da declaração de uma classe parametrizável, e também podemos ver um problema gráve gerado por esta parametrização, o código das linhas 7 e 8, é válido, já que String é subclasse de Any, mas como não existe nenhuma verificação em tempo de execução, teriamos problemas na linha 9, onde um objeto diferente de string seria colocado no Array, o java resolve este problema com uma verificação em tempo de execução, o que fere um pouco a idéia de tipagem estática da linguagem, em scala é possível mudar a declaração da classe para melhorar um pouco esta verificação, adicionando um pouco de variância na declaração da classe e do método …

(este código não é executável, é apenas um exemplo)
samples/032_typeparams2.scala

1
2
3
4
5
//Declaração parcial da classe Array em Scala
class Array[A+] {
def apply(index: Int): A
def [B <: A]update(index: Int, elem: B)
}

A notação A+ diz que o tipo A só deve ser utilizado em posições covariantes, e na nova assinatura do método update, que agora é um método polimorfico, não é mais utilizado o tipo A, agora utilizamos o tipo B, que é definido na própria assinatura do método, como sendo um sub tipo de A, assim resolvendo o problema de inserir objetos que não são strings, o método só vai aceitar strings ou seus sub tipos.

Scala possui as seguintes notações para variância e limites superiores e inferiores de tipos:

Notação Explicação
A+ A só pode ser utilizado em posições de covariância
A- A só pode ser utilizado em posiçÕes de contravariância
B <: A B é menor ou igual a A, ou seja, B é A ou um de seus subtipos
B >: A B é maior ou igual a A, ou seja, B é A ou um super tipo deste
B <: A >: C B é um subtipo de A que é um super tipo de C

Isto torna as coisas bastante flexíveis para definição de tipos e de parâmetros para métodos, permitindo que recebamos apenas o que sabemos como utilizar.
Mas tenham cuidado com os métodos polimorficos, eles são poderosos mas podem deixar o código um tanto quanto difícil de ser lido.

Vamso dar uma olhada em um exemplo um pouco mais complexo agora, retirado da documentação da linguagem e explicar os detalhes dele.

samples/033_typeparams3.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Stack[+A] {
  def push[B >: A](elem: B): Stack[B] = new Stack[B] {
    override def top: B = elem
    override def pop: Stack[B] = Stack.this
    override def toString() = elem.toString() + " " +
                              Stack.this.toString()
  }
  def top: A = error("no element on stack")
  def pop: Stack[A] = error("no element on stack")
  override def toString() = ""
}
 
var s = new Stack[Any]().push("hello");
s = s.push(new Object())
s = s.push(7)
Console.println(s)

Na linha 2, definimos que o parâmetro do método push vai ser qualquer super tipo do tipo inicial, basicamente isto faz o método aceitar qualquer coisa, por que a declaração vai até Any, neste método é criada uma nova classe, descendente de Stack, que sobre escreve os métodos “top”, “pop” e “toString”.
Na linha 13 criamos uma nova instância de Stack[Any], em que o método push vai ser declarado como push(Any), já que não existe um super tipo de Any, e push(Any) vai aceitar qualquer valor como parâmetro.

Bom, não sei se neste artigo ajudei ou atrapalhei, mas se tiverem dúvidas, criticas ou sugestões, como sempre é só deixar um comentário que vou tentar responder o mais rápido possível.