Haskell/Tipos básicos
Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Em programação, tipo são usados para agrupar valores similares, com características similares, dentre de uma certa categoria. Em Haskell, seu sistema de tipos é uma ferramenta poderosa usada para evitar muitos erros triviais dentro de um programa.
Introdução
[editar | editar código-fonte]Quando se cria um programa, geralmente temos que lidar com várias entidades diferentes. Por exemplo, a operação . O que são 2 e 3? Números. E o sinal de mais no meio deles? Não é um número, e trata-se de um símbolo que representa a operação de adição que queremos fazer entre 2 e 3.
Agora, imagine um programa que lhe peça seu nome, e o cumprimente com uma mensagem de "Olá". Nem seu nome, nem a mensagem são números, mas trata-se de palavras. São, portanto, texto. Em programação, quando nos referimos a texto, geralmente dizemos String, que significa "fio" ou "cadeia", em inglês. É uma forma curta de dizer "uma cadeia de caracteres".
Um banco de dados é um bom exemplo para ilustrar os diferentes tipos de dados possíveis. Imagine que temos uma tabela de contatos, como uma agenda. O conteúdo poderia ser o seguinte:
Primeiro nome | Sobrenome | Endereço de correspondência | Número do telefone |
João | Romão | Ladeira do Livramento, 77, Cortiço, Rio de Janeiro | 218444 |
Severino | Retirante | Av. Capibaribe-Rio, 1025, Recife | 813586 |
Os campos de cada entrada possuem um valor diferente. João é um valor. Ladeira do Livramento, 77, Cortiço, Rio de Janeiro e 218444 são outros dois. Em termos de seus tipos, "Primeiro nome" e "Sobrenome" são textos, então são do tipo String.
À primeira vista, poderíamos classificar "Endereço de correspondência" também como texto. Entretanto, há diferentes formas de se interpretar um endereço como este. O primeiro nome pode ser o nome do prédio, ou o nome da rua, enquanto números subsequentes podem ser o número do imóvel ou de seu lote escriturado. E as palavras seguintes podem ser o nome do bairro ou da cidade. É possível também que o endereço comece por "Caixa Postal", e neste caso, ele sequer indica um endereço final, mas apenas uma caixa de correspondência em alguma agência postal. Portanto, cada parte do endereço tem um significado próprio.
A princípio poderíamos dizer que o endereço trata-se de um String. De fato, ele o é, mas aí perderíamos muitas informações importantes. Quando algo é definido com String, dizemos que se trata de uma simples sequencia de caracteres, enquanto que, se pudermos encaixá-lo numa categoria mais específica, teremos bem mais informações a cerca de seu conteúdo. Por exemplo, se soubermos que um String trata-se de um Endereço, já temos muito mais informações a seu respeito, e podemos agora interpretá-lo e manipulá-lo de maneira mais apropriada.
Essa mesma lógica também pode ser aplicada aos números de telefone. Podemos especificar um tipo chamado NúmeroTelefone e assim, quando houver algum número desse tipo, poderíamos buscar padrões como códigos de área ou de país. Um outro motivo para não tratá-los como números comuns, é que não convém fazermos operações artiméticas com eles: o que você espera conseguir ao multiplicar um NúmeroTelefone por 100, ou dividí-lo pela metade? É possível que o resultado nem seja um número de telefone válido. Além disso, não é aceitável perder, omitir ou arredondá-los pelo mesmo motivo.
A utilidade dos tipos
[editar | editar código-fonte]Mas por que é que separar e categorizar coisas nos ajuda a programar? Uma vez que temos os tipos definidos, podemos especificar o que se pode ou não fazer com os valores. Isso ajuda bastante a evitar erros quando tempos programas muito extensos.
Usando o comando interativo :type
[editar | editar código-fonte]Vamos explorar o funcionamento dos tipos usando uma sessão do GHCi. O tipo de uma expressão pode ser verificado usando-se o comando :type
, ou :t
. Tente com booleanos e expressões booleanas:
Prelude> :type True True :: Bool Prelude> :type False False :: Bool Prelude> :t (3 < 5) (3 < 5) :: Bool
O símbolo :: aparece em todos os resultados, e é lido com o "é do tipo", e é seguido por uma assinatura de tipo, ou anotação de tipo.
:t
nos mostra que valores de verdadeiro (True
) ou falso (False
) são do tipo Bool
. Até mesmo expressões possuem um tipo definido: quando elas forem avaliadas, elas resultarão num valor do tipo Bool
. Talvez já esteja claro para você, mas vale notar que Bool
é o tipo de dado escolhido para representar valores do tipo sim ou não. Por exemplo: se um valor for encontrado ou não, se um botão foi pressionado ou não.
Caracteres e strings
[editar | editar código-fonte]Vamos agora testar :t
com outros coisas. Em Haskell, um único caractere é representado cercando-o por aspas simples:
Prelude> :t 'H' 'H' :: Char
Temos, então, que caracteres são do tipo Char
, abreviação de character (caractere, em inglês). Agora, em vez de usarmos um único caractere, para representar uma cadeia deles, devemo usar aspas duplas:
Prelude> :t "Olá, mundo" "Olá, mundo" :: [Char]
De novo, temos Char
, só que cercado por colchetes. [Char]
significa uma lista de caracteres. Haskell considera que toda cadeia de caracteres é uma lista. Lista, em geral, são bem importantes em Haskell, e falaremos sobre elas mais adiante.
- Exercício
- Use
:type
seguido de um valor como"H"
no GHCi. Perceba que usamos aspas duplas aqui. O que acontece? Por quê? - Use
:type
seguido de um valor como'Olá, mundo'
no GHCi. Perceba que usamos aspas simples aqui. O que acontece? Por quê?
Haskell nos permite criar sinônimos de tipos, que funcionam como sinônimos na linguagem humana: palavras diferentes que tem o mesmo significado. Esses sinônimos são nomes alternativos para os tipos. Por exemplo, String é definido como um sinônimo para [Char], e por isso podemos substituir um pelo outro:
"Olá, mundo" :: String
O código acima é perfeitamente válido, e é muito mais claro para humanos ler String
uma única vez, do que ler [Char]
e interpretar que trata-se de uma lista de caracteres e, portanto um string.
Tipos das funções
[editar | editar código-fonte]Até agora vimos os tipos de alguns valores (strings, booleanos, caracteres etc.). Também vimos que tais tipos nos ajudam a categorizar e descrever certos valores de forma mais convenientes. Agora, veremos o que faz o sistema de tipos de Haskell tão importante e poderoso: funções possuem tipos também.
Exemplo
[editar | editar código-fonte]Podemo inverter um booleano usando a função not
, como já vimos. Para sabermos o tipo de uma função, temos levar duas coisas em consideração: os tipos de seus argumentos, e o tipo do valor que ela retorna, isto é, o tipo de sua saída. Para o caso de not
as coisas são bem simples: ela recebe um valor do tipo Bool
e retorna outro do tipo Bool
. A notação que usamos em Haskell é a seguinte:
not :: Bool -> Bool
Essa linha pode ser lida como: "not é do tipo que recebe valores do tipo Bool e retorna valores do tipo Bool". Em outras palavras: uma função de Bool (a entrada) em Bool (a saída).
No GHCi, podemos usar :t
em funções também:
Prelude> :t not not :: Bool -> Bool
Portanto, o tipo de uma função sempre é descrito em termos dos tipos de sua entrada e do tipo de sua saída.
Exemplo: chr
e ord
[editar | editar código-fonte]A representação de texto sempre foi um problema para computadores. Em sua linguagem mais básica, um computador só pode interpretar dados binários: 1 e 0. Para representar um texto, cada caractere deve primeiramente ser convertido a um número, que pode ser convertido para sua forma binária e depois interpretada pelo computador. A conversão para a forma binária é uma tarefa trivial para a máquina, portanto, só nos interessa a conversar de um caractere para sua forma numérica.
A forma mais simples para tal tarefa seria escrever todos os caracteres possíveis e enumerá-los. Por exemplos, se fizermos "a" corresponder a 1, "b" poderia ser 2, "c" seria 3, e assim por diante. Haskell já possui um padrão de enumeração de caracteres definido, e seria tedioso buscar em sua tabela os valores numéricos sempre que precisarmos deles. Por conveniência, já temos duas funções que fazem este trabalho: chr
(uma abreviação de char), e ord
.[nota 1] Vamos investigar os tipos de chr
e ord
:
chr :: Int -> Char
ord :: Char -> Int
Como já sabemos o que Char
significa, vamos explicar sobre Int
. Este tipo se refere a números inteiros, e é apenas um dentre alguns outros tipos de números definidos em Haskell. Na verdade, existe também mais de um tipo para representar números inteiros em Haskell. A assinatura de tipo de chr
nos diz que ela recebe um argumento do tipo Int
, um número inteiro, e avalia um resultado do tipo Char
. O caso de ord
é o oposto: recebe um argumento do tipo Char
e retorna um do tipo Int
. Analisando ambas assinaturas, fica claro qual das funções converte um caractere num número (ord
), e qual faz a operação inversa (chr
).
Agora, alguns exemplos. Estas funções não estão nativamente disponíveis, ao abrir uma sessão do GHCi, precisamos usar o comando :module Data.Char
(ou usar :m
) para carregar o módulo Data.Char, que é onde elas estão definidas.
Prelude> :m Data.Char Prelude Data.Char> chr 97 'a' Prelude Data.Char> chr 98 'b' Prelude Data.Char> ord 'c' 99
Funções de mais de um argumento
[editar | editar código-fonte]Qual seria o tipo de uma função com mais de um argumento?
xor p q = (p || q) && not (p && q)
xor
é a função "ou exclusivo", que avalia se apenas um dos argumentos for True
. Se ambos ou nenhum forem verdadeiros, ela retorna False
.
O método básico para descrever o tipo de uma função de mais de um argumento é simplesmente escrever os tipos de cada argumento, em linha, e em sequência (primeiro p
, depois q
, por exemplo) e, por último, adicionar o tipo da saída. Depois basta intercalá-los com ->
.[nota 2]
-
Escreva o tipo dos argumentos de entrada, em sequência, e o tipo da saída no fim. A definição de
xor
já nos diz que ela avalia se seus argumentos sãoTrue
ouFalse
. Então eles só podem ser do tipoBool
. Sua saída também é do mesmo tipo,Bool
:Bool Bool Bool ^^^^ p é do tipo Bool ^^^^ q é do tipo Bool ^^^^ a saída também é do tipo Bool
- Intercale os tipos usando
->
:Bool -> Bool -> Bool
Portanto, a assinatura de xor
é:
xor :: Bool -> Bool -> Bool
Ela pode ser lida como: "xor é do tipo que recebe dois Bool e retorna um Bool".
Exemplo: abrirJanela
[editar | editar código-fonte]A medida que alguém aprofunda seus conhecimentos de programação, alguns termos técnicos tornam-se triviais. Um deles, são as chamadas bibliotecas, que são arquivos de código com um determinado propósito, mas que podem ser usados por diferentes programas. E um dos tipos de bibliotecas mais comuns é o de GUI (Graphical Unser Interface, ou interface gráfica de usuário, em inglês). O que se espera encontrar neste tipo de biblioteca são funções para criar, botões, menus e interação com mouse e teclado, por exemplo. Uma possível função seria abrirJanela
, que provavelmente abre uma nova janela quando executada. A assinatura de uma função como esta poderia ser:
abrirJanela :: JanelaTitulo -> JanelaTamanho -> Janela
Não precisamos entender a fundo a definição destes tipos, mas seus nomes nos ajudam a entender o que é necessário para criar uma janela. É bastante provável também que eles estejam definidos dentro da biblioteca da própria biblioteca que possui define abrirJanela
.
Vamos investigar por partes. Lembre-se do que explicamos na seção anterior: o último tipo descrito na assinatura de uma função é o tipo de sua saída. Portanto, quantos argumentos abrirJanela
exige? Dois argumentos. Quais são os seus tipos? JanelaTitulo
e JanelaTamanho
. E qual o tipo da saída? É Janela
. Assim, podemos entender que: "abrirJanela
é uma função que cria uma nova janela e que para isso exige um título e um tamanho".
Perceba que não é preciso já ter usado uma função para entender o que ela faz: quando usamos tipos cujos nomes são claros o bastante, a assinatura da função nos dá dicas de seu funcionamento. É um bom hábito testar as funções com :t
no GHCi antes de usá-la e, quanto mais você o fizer, melhor será sua intuição para interpretar os tipos das funções.
Exercícios |
---|
Tente deduzir quais as anotações de tipos das seguintes funções. Se alguma delas envolver números, suponha que sejam do tipo Int.
|
Anotações de tipo em arquivos de código
[editar | editar código-fonte]Agora que já entendemos um pouco o que são os tipos e no que eles podem nos ajudar, vejamos como fazer este tipo de anotação num código fonte de um programa que você está escrevendo. Considere a função xor
que definimos antes. Se quisermos defini-la dentro de um arquivo, e quisermos anotar seu tipo, basta escrever:
xor :: Bool -> Bool -> Bool
xor p q = (p || q) && not (p && q)
Só isso. Para maior clareza, é comum que a anotação apareça imediatamente antes da definição, então é recomendável que você se atenha a esta convenção.
Além de facilitar que humanos entendam o funcionamento da função, as anotações de tipo tão são fundamentais para que o compilador as interprete corretamente, como veremos a seguir.
Inferindo tipos
[editar | editar código-fonte]Como dissemos antes, funções possuem tipo. É importante destacar que elas sempre possuem tipo. Mas como é que escrevemos as funções do capítulo anterior sem nenhum assinatura? Isso é possível porque o compilador consegue inferir o tipo de uma função baseando-se nas operações que ela executa. Vejamos o seguinte caso:
ehL c = c == 'l'
ehL
é uma função de um argumento e que retorna o resultado da expressão c == 'l'
. Mesmo sem anotarmos explicitamente o tipo de c
e da saída, podemos tentar deduzir a assinatura de ehL
. Veja:
- É uma função de um argumento,
c
. E toda função em Haskell tem apenas uma saída.
_ -> _
- A saída é resultado da operação
c == 'l'
. Como já vimos,(==)
é uma comparação de igualdade, e comparações geralmente resultam em respostas do tipo sim ou não, o que pode ser representado pelo tipo Bool. Na verdade, já vimos(==)
em Verdadeiro ou falso, então sabemos que sua saída é um Bool, com certeza.
_ -> Bool
- A função
(==)
comparac
com'l'
. Sabemos que não faz sentido comparar coisas de tipos diferentes. O valor'l'
é do tipo Char, portanto, só faz sentido quec
seja do mesmo tipo.
Char -> Bool
Se quiser verificar o que acabamos de fazer, defina ehL
no GHCi sem uma assinatura e depois use :t
para verificar seu tipo:
Prelude> let ehL c = c == 'l' Prelude> :t ehL ehL :: Char -> Bool
O compilador realiza um processo bastante parecido com este para definir o tipo de uma função. Entretanto, ele pode não resultar na assinatura que você espera ou deseja, ou sequer conseguir chegar a uma assinatura final. Por isso, é recomendável sempre escrever a anotação de tipo de suas funções:
ehL :: Char -> Bool
ehL c = c == 'l'
Há também outros bons motivos para incluí-las no código:
- Documentação: como já vimos, anotações de tipo ajudam a entender o que uma função faz. Documentar um programa, é justamente descrever o que suas partes fazem e como funcionam. Claro que comentários ajudam, mas em Haskell as anotações de tipo são tão importantes quanto comentários.
- Debugging: muitos bugs, ou erros encontrados nos programas são causados quando se uma função de forma errada. As anotações de tipo ajudam você e o compilador a descobrir se uma função está sendo usada de forma correta, verificando os tipos das muitas entradas e saídas que seu programa pode gerar. Quando declaramos as anotações e ocorre algum erro de incompatibilidade de tipos, o compilador sempre mostra uma mensagem dizendo onde está o problema. Se os tipos forem omitidos, o compilador pode inferi-las de uma forma que funcionem num parte, mas que causem outros erros inesperados durante a execução, ou outro erro de compilação em outra parte.
Vejamos um exemplo simples de debugging. Tente o seguinte no GHCi:
Prelude> "olá" ++ " mundo" "olá mundo" Prelude> "olá" + " mundo" <interactive>:2:1: error: * No instance for (Num [Char]) arising from a use of `+' * In the expression: "ola" + " mundo" In an equation for `it': it = "ola" + " mundo"
Acabamos de introduzir a função (++)
, que nada mais faz que concatenar os dois Strings: "ola"
e " mundo"
. Se por um erro de digitação você escrever + em vez de ++, a mensagem de erro acima apareceria. De forma resumida, ela nos diz que (+)
só está definida para dados Num
, e que estamos tentando aplicá-la a dados do tipo [Char]
, ou String, como já vimos.
É importante dizer que a checagem de tipos não previne completamente a ocorrência de erros. Na verdade, ainda podemos escrever uma função de uma forma que não funcione como a desejada. Mas mesmo aí, pensar nas operações que podemos ou não fazer como cada tipo de dado, nos ajuda guiar nossa lógica programação.
Notas
[editar | editar código-fonte]- ↑ Não é exatemente assim que
chr
eord
funcionam, mas descrição é válida por enquanto. - ↑ Cobriremos mais sobre este assunto no capítulo sobre funções de alta ordem.