Programação, Scala

Tuplas e listas básicas em Scala

Em diversas circunstâncias, criar classes inteiras para acomodar dados simples pode tornar o código desnecessariamente complicado. Talvez seja preciso armazenar apenas a matrícula de alunos e sua menção no semestre em uma lista.

Por exemplo, para criar uma lista de nomes e emails seria um exagero criar uma classe dedicada para armazenar os pares de dados (*).

Para agrupar dados simples, Scala (como a maioria das linguagens funcionais) oferece a funcionalidade de tuplas. Tuplas são estruturas de dados que permitem agrupar dados estruturados com sintaxe extremamente simples: parenteses delimitam uma tupla, e cada ítem listado entre parenteses tem um significado de acordo com a ordem em que aparece.

(*OBS: As tuplas de Scala são traduzidas em classes Java. Logo o “overhead” de criação de uma classe existe, porém o código-fonte fica mais limpo ao abstrair a implementação real.)

Tuplas são como objetos onde os campos não tem nomes próprios, são referidos apenas como “primeiro valor”, “segundo valor” e assim por diante. Por este motivo são chamadas de conjuntos ordenados, pois a ordem dos itens define seu significado. No caso de tuplas de 2 ítens temos os pares ordenados, com 3 membros temos trios ordenados, até n-tuplas, con n membros ordenados. Repare que “ordenados” aqui não implica uma ordem lexicográfica, e sim o fato da posição do membro na tupla ser relevante.

Exemplo:
val tupla = ("um","dois","tres")
tupla._3 //> res0: String = tres
val alunosAprovados =
Vector[(String,String)](("Mario Soares","MM"),("Janaina Paiva","SS"),("Johnny Souza","MS"))
//> alunosAprovados : scala.collection.immutable.Vector[(String, String)]
//> = Vector((Mario Soares,MM), (Janaina Paiva,SS), (Johnny Souza,MS))
val mencaoJanaina = alunosAprovados(1)._2 //> mencaoJanaina : String = SS

Na primeira linha definimos uma tupla simples, composta por 3 membros do tipo String. Os membros das tuplas são referenciados com a sintaxe _N onde N é sua posição na tupla, portanto tupla._3 retorna o terceiro item, no caso tendo o valor “tres”.

Outra forma de acessar os membros de uma tupla é através de pattern matching:
alunosAprovados(2) match {
case (nome, mencao) => println("Aluno " + nome + " obteve mencao " + mencao)
} //> Aluno Johnny Souza obteve mencao MS

Na definição de alunosAprovados demonstramos que tuplas são tipos de dados, criando um Vector de tuplas. Ou seja, a tupla é aceita como uma classe de dados que pode ser armazenada em vetores, passada a funções e assim por diante.

No caso, o tipo da tupla do Vector alunosAprovados é Tuple2 com assinatura (String, String). As tuplas tem a assinatura de tipo dos tipos de dados que a compõem. O Vector só pode armazenar objetos do mesmo tipo, ou seja, é uma estrutura de dados homogênea. Todas as tuplas armazenadas no Vector alunosAprovados devem ter o mesmo tipo: Tuple2 com assinatura (String,String). Tentar armazenar uma nota numérica no lugar da menção, no exemplo acima, geraria um erro de compilação. A tupla (“Eduardo Souza”, 9.3) não seria aceita no Vector alunosAprovados, por exemplo.

Sendo um tipo de dados, as tuplas são um valor e não uma coleção de valores. Quando uma função recebe uma tupla de 3 valores, a função recebe um parâmetro, não 3.

Exemplo:
def multiplica(dados: (Int,Int)): Int = {
dados._1 * dados._2
} //> multiplica: (dados: (Int, Int))Int
multiplica((3,4)) //> 12
// o seguinte exemplo tambem funciona, mas continua sendo um parametro apenas!
multiplica(3,4) //> 12

A função multiplica recebe uma tupla de tipo (Int, Int) como parâmetro. Note que não são dois parâmetros, a tupla que é passada à função é armazenada em uma só variável: dados. multiplica é uma função que trabalha os dados contidos em uma tupla, a qual é passada à função. Observe que a chamada tem dois parenteses adicionais. Na última linha do exemplo acima, observe o trabalho que o compilador Scala realiza por nós: ao passarmos dois parâmetros do tipo Int, o compilador realiza pattern matching nos valores passados e identifica uma tupla do tipo (Int, Int). Isso não significa que o método passou a ter assinatura multiplica(Int, Int), ele continua tendo apenas um parâmetro, foi apenas o trabalho interno do compilador Scala que identificou o tipo e desmembrou a tupla nos inteiros 3 e 4 através de pattern matching. Scala realiza pattern matching em todas as parametrizações de funções.

De fato, a “mágica” que o compilador Scala realiza por trás das cortinas faz com que os parâmetros de funções sejam tratados como tuplas. E como todas as tuplas em Scala, todas as funções são limitadas a apenas 22 membros. Qualquer tupla, ou qualquer função, cuja assinatura tenha mais que 22 membros causará um erro de compilação:
val testeLimite = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
//passa OK
val passaLimite = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
// nao compila
// ERRO: object Tuple23 is not a member of package scala

O erro acima pode surpreender aqueles que estão iniciando no estudo de Scala. Repare que a mensagem do compilador é que não existe um tipo Tuple23. A partir dessa mensagem o programador pode inferir que cada tupla tem um tipo de acordo com o número de membros. Esse é, de fato, o caso! Tuplas com 2 membros, ou pares ordenados, são do tipo Tuple2. A biblioteca padrão de Scala define tipos Tuple{1 a 22}. Como não existe programação dinâmica em Scala, é impossível expandir dinâmicamente o tamanho de tuplas, seria preciso definir tipos adicionais para comportar mais que 22 parâmetros.

Conclusão
Tuplas são um idioma comum em linguagens funcionais, e Scala as implementa como objetos do tipo TupleN com 1 <= N <= 22. Os membros das tuplas são referenciados através de sua posição na estrutura, através do underscore e N, assim: Tuple._N. Para obter o N’esimo termo de uma tupla acessamos através da chamada: minhaTupla._N.

Standard