Haskell/Declaração de tipos
Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Em Haskell, e muitas outras linguagens de programação, não estamos limitados a trabalhar apenas com o tipos de dados predefinidos. Na verdade, há muitas vantagens em definirmos nossos próprios tipos:
- O código pode ser escrito em termos do problema a ser resolvido, o que os torna fácil de serem desenvolvidos, escritos e entendidos.
- Dados correlacionados podem ser organizados e trabalhados de formas mais convenientes do que apenas tratá-los como elementos de listas ou n-uplas.
- Podemos usar casamento de padrões e o sistema de tipos de Haskell em todo seu potencial, fazendo ambos funcionarem com nosso próprios tipos.
Em Haskell, temos três maneiras básicas para declaração de tipos:
- Usando data para novos tipos.
- Usando type para declarar sinônimos de tipos, isto é, nomes alternativos para tipos já existentes.
- Usando newtype para definir novos tipos equivalentes a outro existente.
Neste capítulo usaremos apenas data e type. Quanto a newtype, este método será discutido em capítulos futuros, quando virmos como ele pode ser útil.
data e funções de construção
editarUsamos data para criar novos tipos baseando-nos principalmente em outros já existentes. Por exemplo, para uma lista simples de datas de aniversário:
- Um aniversário pode ser de casamento ou aniversário, por exemplo.
- Se for de nascimento, devemos armazenar o nome de uma pessoa. Este dado deve ser um String.
- Se for de casamento, devemos armazenar o nome dos dois cônjuges. Estes dados devem ser Strings.
- Uma data contém três números: dia, mês e ano.
- É possível que usar a ordem ano-mês-dia ajude na ordenação das datas, ou evite equívocos de interpretação. Estes dados devem ser Int.
Agora que já definimos o que queremos armazenar, podemos definir o tipo Aniversario usando data:
data Aniversario = Nascimento String Int Int Int -- nome, ano, mês, dia
| Casamento String String Int Int Int -- nome 1, nome 2, ano, mês, dia
O tipo Aniversario possui dois construtores de tipo: Nascimento e Casamento, sendo que cada um deles exige certos valores, como já vimos no nosso esboço. Estes construtores também pode ser chamados de funções de construção ou funções construtoras.
Abra o GHCi e tente:
Prelude> data Aniversario = Nascimento String Int Int Int | Casamento String String Int Int Int Prelude> :t Nascimento Nascimento :: String -> Int -> Int -> Int -> Aniversario
Isso quer dizer que Nascimento pode ser usado como função, cujos argumentos são um String e três Ints, e cuja saída é um dado do tipo Aniversario. O mesmo vale para Casamento, que possui cinco argumentos:
Prelude> :t Casamento Casamento :: String -> String -> Int -> Int -> Int -> Aniversario
Já que podemos tratar construtores de tipos como funções, podemos usá-los como funções normais. Por exemplo, para armazenar o aniversário de João Romão, nascido no dia 3 de julho de 1968:
joaoRomao :: Aniversario
joaoRomao = Nascimento "João Romão" 1968 7 3
Ele se casou com Maria Romão no dia 4 de março de 1987:
romaoCasamento :: Aniversario
romaoCasamento = Casamento "João Romão" "Maria Romão" 1987 3 4
Apesar de armazenarem dados diferentes, joaoRomao
e romaoCasamento
são ambos do tipo Aniversario
. Isso quer dizer que podemos armazená-los juntos numa única lista:
aniversariosDeJoaoRomao :: [Aniversario]
aniversariosDeJoaoRomao = [joaoRomao, romaoCasamento]
Também poderíamos ter criado a lista sem variáveis intermediárias, mas o código poderia não ficar tão legível:
aniversariosDeJoaoRomao :: [Aniversario]
aniversariosDeJoaoRomao = [Nascimento "João Romão" 1968 7 3, Casamento "João Romão" "Maria Romão" 1987 3 4]
É importante saber que cada novo tipo declarado com a data deve possuir pelo menos um construtor. Nestes casos, você também verá que é conveniente que o construtor tenha mesmo nome que o tipo:
data Dado1 = Dado1 -- Dado1 possui apenas um construtor e não recebe nenhum valor adicional
data Dado2 = Dado2 Int -- Dado2 possui apenas um construtor e recebe um valor adicional do tipo Int
Acessando valores armazenados
editarUsar novos tipos de dados só é útil se pudermos acessar o conteúdo que eles armazenam. Só podemos fazer isso se criarmos funções apropriadas para tais tarefas.
Para nosso exemplo acima, uma operação muito útil seria extrair os nomes e as datas armazenadas, e poder apresentá-los em forma de String. Para isso, criamos uma função chamada mostrarAniversario
. Como temos que converter os valores numéricos das datas em String, podemos criar também a função auxiliar mostrarData
que usa show
para converter os números:
mostrarData :: Int -> Int -> Int -> String
mostrarData a m d = show a ++ "-" ++ show m ++ "-" ++ show d
mostrarAniversario :: Aniversario -> String
mostrarAniversario (Nascimento nome ano mes dia) =
nome ++ " nasceu em " ++ mostrarData ano mes da
mostrarAniversario (Casamento nome1 nome2 ano mes dia) =
nome1 ++ " casou-se com " ++ nome2 ++ " em " ++ mostrarData ano mes dia
Usamos casamento de padrões para criar mostrarAniversario
. São dois padrões: um deles casa com dados construídos com Nascimento
, o outro casa com dados construídos com Casamento
. Depois de definir o construtor, definimos as variávies de vínculo (binding variables, em inglês). Estes são os nomes que damos para cada valor contido no dado, como nome
, ano
ou mes
, por exemplo. Poderíamos ter escrito Nascimento n a m d
e depois usado n
, a
, m
, e d
para nos referirmos a cada valor ao longo da definição da função, mas usar nomes mais expressivos deixa o código mais claro.
Os parênteses em torno do nome do construtor e das variáveis vinculadas são obrigatórios, senão, o compilador não criaria mostrarAniversario
como sendo uma função de um argumento só. Também é importante saber que a expressão dentro dos parênteses não é uma chamada para as funções de construção, isto é, Casamento nome1 nome1 ano mes dia
não será avaliada, pois trata-se apenas de uma descrição do padrão.
Por fim, podemos carregar tudo no GHCi e testar:
*Main> putStrLn (mostrarAniversario joaoRomao) João Romão nasceu em 1968-7-3 *Main> putStrLn (mostrarAniversario romaoCasamento) João Romão casou-se com Maria Romão em 1987-3-4
Exercícios |
---|
Observe a função
|
Criando sinônimos de tipo com type
editarComo dito no começo deste capítulo, ter mais clareza é um dos objetivos que nos leva a criar novos tipos. Neste sentido, seria bom poder deixar claro que os String em Aniversario são nomes e ainda poder manipulá-los como um String comum. Para isso, podemos usar a palavra-chave type para criar um sinônimo:
type = Nome = String
Esta linha quer dizer que Nome agora é um sinônimo de String
. Isso quer dizer que qualquer função que aceite um dado String também aceitará um do tipo Nome, e vice-versa. O lado direito da expressão também pode ser um tipo mais complexo. O próprio tipo String é sinônimo de uma lista de Char, e podemos confirmar isso usando o comando :info
(ou :i
) no GHCi para obter informações sobre os tipos e outras funções:
Prelude> :i String type String = [Char] -- Defined in `GHC.Base'
GHC.Base é onde está definido o Prelude. Podemos fazer algo semelhante para uma lista de aniversários:
type LivroDeAniversarios = [Aniversario]
Sinônimos são, em geral, apenas uma conveniência. Eles geralmente nos ajudam a esclarecer o papel tipos complexos -- como listas, n-uplas ou funções -- dentro de um certo contexto. Entretanto, deve-se evitar o uso em excesso dos sinônimos, pois o código pode se tornar bastante confuso: um código que usa vários sinônimos diferentes para um mesmo tipo pode dificultar o seu entendimento.
Exercícios |
---|
Reescreva a declarção de Aniversario usando o sinônimo Nome, e mantendo o uso de Data. |
Sintaxe de registros
editarO uso de data ainda nos permite definir tipos como sendo registros. Por exemplo, o tipo Data definido como registro seria:
data Data = Data {ano :: Int, mes :: Int, dia :: Int}
Para definir os valores de cada campo, usamos a mesma sintaxe, sendo que a ordem dos campos não importa:
fimDoSeculoXIX = Data 1900 12 31
fimDoSeculoXXI = Data {dia = 31, mes = 12, ano = 2100}
Esta definição cria, também, três funções de acesso: ano
, mes
e dia
. Podemos ver seus tipos no GHCi:
Prelude> data Data = Data {ano :: Int, mes :: Int, dia :: Int} Prelude> :t ano ano :: Data -> Int Prelude> :t mes mes :: Data -> Int Prelude> :t dia dia :: Data -> Int
Isso quer dizer que para sabermos o dia armazenado em fimDoSeculoXXI
, por exemplo, basta escrever dia fimDoSeculoXXI
.
Esta sintaxe também ajuda bastante quando precisamos mudar os valores de alguns campos, em vez de todos:
fimDoSeculoXX = fimDoSeculoXXI {ano = 2000}
Registros são especialmente úteis quando precisamos criar variáveis que armazenem muitos valores. Por exemplo, sem usar a sintaxe de registros, uma agenda de contatos — onde usamos Int para representar números de telefone — poderia ser:
data Contato = Contato
String -- nome
String -- sobrenome
Int -- telefone
String -- endreço
String -- email
type Agenda = [Contato]
O principal problema de Cotato
é que sua definição pode causar confusão sobre o que cada valor deve ser. Usar :i
no GHCi não ajuda muito:
Prelude> :i Contato data Contato = Contato String String Int String String -- Defined at registros.hs:1:1
Se o tipo Contato
fosse criado como um registro, uma vez que damos nomes para cada campo, esse mesmo problema não existiria, pois saberíamos exatamente o que campo deve ser:
data Contato = Contato { nome :: String
, sobrenome :: String
, telefone :: Int
, endereco :: String
, email :: String
}
O GHCi também seria útil neste caso:
*Main> :i Contato data Contato = Contato {nome :: String, sobrenome :: String, telefone :: Int, endereco :: String, email :: String} -- Defined at registros.hs:1:1
Devemos ter cuidado, entretanto, para não criarmos campos de tipos diferentes com mesmo nome dentro de um mesmo programa.[nota 1] Por exemplo, o compilador não aceitaria as seguintes definições, retornando um erro sobre múltiplas declarações de campo1
:
data Dado1 = Dado1 {campo1 :: Int}
data Dado2 = Dado2 {campo1 :: Int, campo2 :: String}
Exercícios |
---|
|
Notas
editar- ↑ Mais especificamente, dentro de um mesmo módulo.