Saltar para o conteúdo

Haskell/Declaração de tipos

Origem: Wikilivros, livros abertos por um mundo aberto.


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

[editar | editar código-fonte]

Usamos data para criar novos tipos baseando-nos principalmente em outros já existentes. Por exemplo, para uma lista simples de datas de aniversário:

  1. Um aniversário pode ser de casamento ou aniversário, por exemplo.
    1. Se for de nascimento, devemos armazenar o nome de uma pessoa. Este dado deve ser um String.
    2. Se for de casamento, devemos armazenar o nome dos dois cônjuges. Estes dados devem ser Strings.
  2. Uma data contém três números: dia, mês e ano.
    1. É 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

[editar | editar código-fonte]

Usar 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 mostrarData. Ela existe apenas para deixar mais clara a definição mostrarAniversario. Note que são passados três argumentos a ela: um ano, um mês e um dia, representados por Ints. Sabemos que não faz sentido passar os argumentos fora de ordem pois são parte de uma data única. Poderíamos, portanto, criar um tipo Data para armazenar apenas datas e evitar confusões:

  • Declare um tipo Data que armazene três Ints para representar ano, mês e dia.
  • Reescreva o tipo Aniversario usando Data.
  • Reescreva as funções mostrarData e mostrarAniversario usando as novas declarações de Data e Aniversario.
  • Redefina joaoRomao e romaoCasamento.

Criando sinônimos de tipo com type

[editar | editar código-fonte]

Como 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

[editar | editar código-fonte]

O 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
  1. Reescreva a função mostrarData usando a declaração de Data na forma de registro.
  2. Trantado-se de registros e dentro de um mesmo módulo, por que dois campos, de dois tipos diferentes, não podem ter o mesmo nome?
  1. Mais especificamente, dentro de um mesmo módulo.