Ir para o conteúdo

Tuplas

Nas funções vistas até agora, todos os parâmetros eram de algum tipo simples. Por exemplo, vejamos a função soma2n que soma 2 números:

soma2n :: Int -> Int -> Int
soma2n a b = a + b

Muitas vezes estes tipos simples precisam ser associados para significar algo de mais alto nível. Por exemplo, seja a função soma2v que soma as coordenadas x e y de 2 pontos representando dois vetores. Como poderíamos definir tal função, já que o resultado deve ter informação tanto sobre a coordenada x quanto y do resultado?

soma2v :: Int -> Int -> Int -> Int -> ██████
soma2v x1 y1 x2 y2 = █████

A resposta está no uso de tipos estruturados, que agregam outros tipos. No caso, a solução ideal para o par ordenado está na forma de uma tupla de dois elementos. Tuplas são geralmente representadas usando a sintaxe (Elem1, Elem2, ... , ElemN), tanto em Haskell como em diversas outras linguagens. Assim, a função soma2v pode ser redefinida como a seguir. Observe que a função define claramente que as tuplas terão 2 elementos e qual a variável associada a cada um dos pontos.

soma2v :: (Int,Int) -> (Int,Int) -> (Int,Int)
soma2v p1 p2 = █████

Para acessar as diferentes coordenadas dentro das tuplas podemos usar as funções fst e snd,1 abreviações para first e second e que retornam o primeiro e o segundo elemento de uma tupla de dois elementos, um par, respectivamente. Isto é,

> snd (1,2)
2
> fst (3,4)
3
> fst (snd ((1,2,3),(4,5)))
4
> fst (1,2,3) <== Erro!

Assim, usando fst e snd, a definição da soma dos vetores fica como se segue:

soma2v :: (Int,Int) -> (Int,Int) -> (Int,Int)
soma2v p1 p2 = ((fst p1) + (fst p2), (snd p1)+(snd p2))

Casamento de padrões

Casamento de padrões pode ser usado para desmembrar uma tupla em suas componentes quando uma função é invocada. Isto é, se um argumento formal é especificado como uma tupla, então o parâmetro correspondente, uma tupla, tem suas componentes casadas com as componentes no parâmetro formal. De forma simplificada, fst e snd poderiam ser definidos com a seguir, usando casamento de padrões.

fst (x,y) =  x

snd (x,y) =  y

Observe que ambas as funções esperam por uma dupla de valores e que uma variável é provida para cada elemento da dupla. Assim, a dupla passada como argumento para a função e desmembrada em duas variáveis que podem ser usados do lado direito da equação. Aplicando a mesma ideia, podemos redefinir a função soma2v como se segue, muito mais intuitiva.

soma2v :: (Int,Int) -> (Int,Int) -> (Int,Int)
soma2v (x1,y1) (x2,y2) = (x1+x2, y1+y2)

Para usar a função, podemos invocá-la de duas formas, usando a notação prefixa ou infixa, com o mesmo resultado.

> soma2v (3,4) (5,4)
(8,8)

>(3,4) `soma2v` (5,4)
(8,8)
Ignorando variáveis

Na prática, mas ainda de forma simplificada, as funções fst e snd são definidos assim.

fst (x,_) =  x

snd (_,y) =  y

Observe que o _ é usado em substituição a um nome para variáveis com as quais não nos importamos, isto é, que não serão usadas no dado escopo. A primeira vista isso poderia parecer uma forma de permitir ao compilador Haskell que otimizasse o uso de recursos, mas a verdade é que o compilador consegue muito bem identificar quais variáveis serão ou não serão usadas do lado direito da equação. O uso de _ é na verdade para permitir que o desenvolvedor demonstre que está ciente de que a variável não foi usada.

Exercício

Considerando uma tupla de 4 elementos, defina 4 funções que, aos moldes de fst e snd, extraiam cada um dos 4 elementos da tupla. Não defina um protótipo.

Resolução
prim (x,_,_,_) =  x
segu (_,y,_,_) =  y
terc (_,_,z,_) =  z
quar (_,_,_,w) =  w

Exercício

Escreva uma função que receba um inteiro como parâmetro e retorne uma tupla como resultado onde o primeiro elemento é um booleano que indica se o número é negativo, e o segundo elemento é o valor absoluto do número.

Resolução
éNeg x = if x < 0 then (True, abs x) else (False, abs x)

éNeg' x = (x < 0, abs x)

Tuplas são como Structs

Tuplas estão para Haskell assim como estruturas estão para outras linguagens. Por exemplo, imagine que se queira armazenar os dados nome, telefone, CPF e endereço de uma pessoa. Poderíamos convencionar que seria usado uma tupla em que cada posição corresponderia a um dos dados. Neste caso, alguns exemplos de funções úteis são mostrados a seguir.

fazPessoa nome telefone cpf endereço = (nome, telefone, cpf, endereço)

pegaNome (nome, _, _, _) = nome

pegaTelefone (_, telefone, _, _) = telefone

trocaTelefone (n, _t, c, e) novoTelefone = (n, novoTelefone, c, e)

Uma observação a ser feita é que, na última função, nomear a variável como _t tem o mesmo efeito que simplesmente _ para o compilador, mas deixa o código mais legível. Outra observação é que mesmo com o uso _t, o código fica rapidamente difícil de se ler, pois o desenvolvedor deve manter em mente qual posição corresponde a qual dado de uma pessoa. Veja o exemplo de uso das funções.

> x = fazPessoa "jose da silva" "12345" "0003003093" "Av das Couves, 14"  
> x
=> ("jose da silva","12345","0003003093","Av das Couves, 14")
> pegaNome x
=> "jose da silva"
> pegaTelefone x
=> "12345"
> y = trocaTelefone x "54321"
> y
=> ("jose da silva","54321","0003003093","Av das Couves, 14")

Imagine estruturas mais complexas, contendo outros dados de cada pessoa, e várias outras estruturas semelhantes, como ordens de serviço, descrição de inventários, cadastro de vendedores, etc. Veremos adiante como definir novos tipos de dados pode facilitar o desenvolvimento e não ter que ficar se lembrando das posições dos valores dentro das tuplas.

O tipo de um tupla

Ao definirmos a função soma2v, definimos que o primeiro parâmetro é uma tupla com duas componentes do tipo Int, ou seja, (Int,Int); este é o tipo do parâmetro. Podemos confirmar esta informação usando :t.

> :t soma2v
soma2v :: (Int, Int) -> (Int, Int) -> (Int, Int)

Para outro exemplo, considere o tipo do resultado da função fazPessoa, uma tupla com quatro String.

> x = fazPessoa "jose da silva" "12345" "0003003093" "Av das Couves, 14"
> :t x
x :: (String, String, String, String)

Observe que tuplas podem ter componentes de tipos diferentes. Por exemplo, podemos ter uma tupla ("XMan: Primeira Turma", 2000::Int, 7.5::Float), cujo tipo é (String, Int, Float).

> :t ("XMan: Primeira Turma", 2000::Int, 7.5::Float)
("XMan: Primeira Turma", 2000::Int, 7.5::Float) :: (String, Int, Float)

Os tipos das componentes de uma tupla podem ter qualquer tipo válido, inclusive outra tupla, como em ("XMan: Primeira Turma", (3::Int, 4::Int, 2000::Int), 7.5::Float), cujo tipo é (String, (Int, Int, Int), Float).

> :t ("XMan: Primeira Turma", (3::Int, 4::Int, 2000::Int), 7.5::Float)
("XMan: Primeira Turma", (3::Int, 4::Int, 2000::Int), 7.5::Float) :: (String, (Int, Int, Int), Float)

Ordem entre tuplas

Dado duas tuplas com mesmo tipo (mesmo tamanho e tipo de suas componentes), podemos compará-las lexicograficamente. Isto quer dizer que uma tupla \(t_1\) é menor que uma tupla \(t_2\) se, considerando posições da esquerda para a direita. Isto é, dado tupla \(t^1\) e uma tupla \(t^2\), se o primeiro elemento da tupla \(t^1\) é menor que o primeiro elemento da tupla \(t^2\), então \(t^1 < t^2\). Caso o primeiro elemento de \(t^2\) seja menor, então \(t^2 < t^1\). E caso os primeiros elementos sejam iguais, a avaliação é repetida para os segundos elementos e assim sucessivamente.

> (1,2) < (1,3)
True
> ('a',2) < ('b',3)
True
> ('a',2) == ('b',3)
False
> ('a',2) > ('b',3)
False
> (1,2) < (1,3,4) <== Erro!

> (1,1,1) < (1,1,1)
False
> (1,1,1) < (1,1,2)
True
> (1,1,1) < (1,2,1)
True
> (1,1,1) < (2,1,1)
True
> (1,1,1) < (0,2,2)
False

Um exemplo do uso desta funcionalidade é na comparação de datas, se as representarmos como tuplas com ano, mês e dia, nesta ordem. Neste caso, duas datas podem ser comparadas diretamente como comparação de tuplas.

> (2000,01,01) < (1999,12,12)
False
> (2000,01,01) < (2001,12,12)
True
> (2000,01,01) < (2000,01,2)
True

A tupla vazia

Por completude, é preciso mencionar que tuplas podem ter qualquer aridade, inclusive zero. Isto é, () é uma tupla válida e a única instância de tuplas de aridade zero. A utilidade desta tupla, denominada Unit, ficará clara mais adiante, quando falarmos sobre entrada e saída.

Os operadores (,...,)

Haskell tem várias instâncias de açúcar sintático. Relativo a tuplas, Haskell provê uma função para a construção das mesmas, como alternativa à sintaxe usada até agora. Por exemplo, para construir a tupla (1,2), pode se usar (,) 1 2, e (,,,,) em vez de (1,2,3,4,5). Esta possibilidade se estende pelo padrão Haskell2010 até a construção de tuplas com 15 elementos, mas o GHC vai até tuplas com cerca de 50 elementos.


  1. Funções definidas no pacote Prelude