Expressões, avaliação e tipos em Haskell
Avaliar expressões por substituição, definir funções por equações e ler tipos predefinidos.
Com dobra x = x * 2, a chamada dobra 4 vale 8. A equação relaciona o argumento com o resultado. O nome x representa o argumento naquela aplicação; não é uma posição de memória que vamos atualizar.
Avaliação por substituição
Toma esta definição:
dobra :: Int -> Int
dobra x = x * 2
A primeira linha é a assinatura de tipo: dobra recebe um Int e devolve um Int. A segunda é a equação. Avaliar dobra (3 + 1) é substituir o argumento na equação: dobra (3 + 1) dá (3 + 1) * 2, que dá 4 * 2, que dá 8. Esta é a avaliação por substituição. A ordem em que Haskell exige esses valores será tratada na avaliação não estrita.
Funções por equações
Uma função define-se por casos, um por linha, cada um com o seu padrão de argumento:
somaQuadrados :: [Int] -> Int
somaQuadrados [] = 0
somaQuadrados (x:xs) = x^2 + somaQuadrados xs
[Int] é o tipo de listas de inteiros. O padrão [] corresponde à lista vazia. O padrão (x:xs) separa uma lista não vazia na cabeça x e na cauda xs. Em [1,2,3], a segunda equação liga x a 1 e xs a [2,3].
A chamada seguinte recebe [2,3], depois [3] e finalmente []. Nesse ponto escolhemos a primeira equação, que devolve 0. A figura conserva as adições pendentes e mostra como chegamos a 14.
Os padrões são tentados de cima para baixo. Aqui são disjuntos, por isso trocar as duas equações não muda o resultado. Um padrão geral como xs antes de [] aceitaria também a lista vazia e esconderia o caso base.
Guardas para condições
Quando a escolha depende de uma condição em vez da forma do argumento, usa guardas, barras verticais lidas de cima para baixo até à primeira verdadeira:
sinal :: Int -> String
sinal n | n > 0 = "positivo"
| n < 0 = "negativo"
| otherwise = "zero"
otherwise é só o valor True, por isso apanha tudo o que chega ali. Usa padrões para a forma dos dados e guardas para condições sobre os valores; misturar os dois na mesma função é normal.
somaQuadrados :: [Int] -> Int
somaQuadrados [] = 0
somaQuadrados (x:xs) = x^2 + somaQuadrados xs
sinal :: Int -> String
sinal n | n > 0 = "positivo"
| n < 0 = "negativo"
| otherwise = "zero"
main :: IO ()
main = do
print (somaQuadrados [1,2,3])
print (sinal 0)Executa o código para ver a saída e os gráficos.
Escolher entre if, guardas e padrões
Para classificar por intervalos, as três formas coincidem quando há duas alternativas. Com três ou mais intervalos, as guardas mostram cada fronteira por ordem:
classe :: Int -> String
classe n | n < 0 = "negativo"
| n == 0 = "zero"
| n < 10 = "pequeno"
| otherwise = "grande"
classe (-2) escolhe a primeira guarda verdadeira e dá "negativo". classe 0 falha na primeira, passa na segunda e dá "zero". classe 5 chega à terceira. Um if aninhado diria o mesmo, mas esconderia cada fronteira dentro de outra expressão; as guardas mostram a ordem das fronteiras.
Padrões literais também separam valores concretos, como um 0 escrito no lugar do argumento. Para intervalos, as guardas evitam enumerar os valores um a um. O símbolo _ é um curinga: aceita qualquer valor sem o ligar a um nome, por isso não pode ser usado no corpo da equação.
Tipos predefinidos
Os tipos que vais usar já na primeira semana:
| Tipo | Valores | Exemplo |
|---|---|---|
Int | inteiros de precisão fixa | 42, -7 |
Integer | inteiros de precisão arbitrária | 10^100 |
Float | aproximações em precisão simples | 3.14 |
Double | aproximações em precisão dupla | 3.14 |
Bool | lógicos | True, False |
Char | carateres | 'a' |
String | listas de carateres | "ola" |
String é só um sinónimo de [Char], por isso "ola" é a lista 'o':'l':'a':[] e todas as funções de listas funcionam em strings. Já Int contra Double é uma distinção a sério: 3 / 2 dá 1.5 em Double, mas com Int precisas de div 3 2, que dá 1. O operador / exige um tipo fracionário; a conversão faz-se com fromIntegral e nunca acontece automaticamente.
div arredonda o quociente para baixo e mod dá o resto correspondente. Para divisor não nulo, vale sempre (x div y) * y + (x mod y) == x. Por exemplo, (-7) div 3 dá -3 e (-7) mod 3 dá 2. Os números em vírgula flutuante têm precisão limitada, por isso nem todos os reais podem ser representados exatamente.
Pares e nomes locais
Uma lista tem elementos do mesmo tipo. Um par pode combinar tipos diferentes: ('a',True) :: (Char,Bool). fst escolhe a primeira posição e snd a segunda. (3) é apenas o número entre parênteses; só a vírgula cria um par. O valor () tem o tipo unitário (), usado quando uma ação não precisa de devolver informação.
where dá nomes a cálculos usados na equação. let ... in ... cria esses nomes dentro de uma expressão:
quadradoSoma :: Int -> Int -> Int
quadradoSoma x y = total * total
where total = x + y
quadradoSomaLocal :: Int -> Int -> Int
quadradoSomaLocal x y = let total = x + y in total * total
As duas funções dão 25 para 2 e 3, porque total é um nome local, não uma variável que recebe atualizações. A indentação delimita as definições: alinha nomes do mesmo bloco e indenta o que pertence a uma equação.
Aplicação, parênteses e domínio
A aplicação de funções tem prioridade sobre os operadores. Com dobra x = 2*x, dobra 3 + 1 dá 7, enquanto dobra (3+1) dá 8. A aplicação associa à esquerda: f x y é (f x) y. Para passar um número negativo como argumento, escreve f (-3); f -3 é uma subtração.
As duas alternativas de um if têm de ter o mesmo tipo e o else é obrigatório. if n > 0 then "positivo" else 0 falha antes da execução, porque mistura String e um valor numérico. Guardas permitem mais alternativas, mas não dispensam cobertura dos casos.
Uma equação bem tipada pode ter um domínio limitado. area a b c = sqrt (s*(s-a)*(s-b)*(s-c)), com s=(a+b+c)/2, pressupõe lados positivos que respeitam as desigualdades do triângulo. Para 1,2,4, temos s=3.5 e s-c=-0.5, pelo que o radicando é negativo. Se a entrada vem do utilizador, valida primeiro ou devolve Maybe Double.
Exercícios
Aplicação antes da soma
Com dobra x = 2*x, quanto vale
dobra 4 + dobra (1+2)?
Pista
Resolve os dois argumentos de dobra separadamente.
Solução
dobra 4 + dobra (1+2) = 8 + 2*3 = 14.
Erros frequentes
Não incluis a soma exterior no argumento da primeira dobra.
Um programa que compila pode falhar
Qual das expressões tem tipo mas falha quando o resultado é pedido?
Pista
Distingue rejeição pelo compilador de falha na avaliação.
Solução
head [] tem um tipo e viola a
pré-condição de lista não vazia. take 0 [1..] não exige
nenhum elemento.
head ([] :: [Int]). O tipo é Int, mas não há primeiro elemento.1 + False. Não há instância Num Bool; falha na tipagem.take 0 [1..]. Devolve a lista vazia sem pedir a lista infinita.
Erros frequentes
Não confundas uma estrutura infinita com uma expressão que precisa de a percorrer toda.
Cobrir a lista vazia
Define caudaSegura com padrões. Os testes devem produzir []
, [] e [2,3].
caudaSegura :: [a] -> [a]
caudaSegura xs = undefined
main :: IO ()
main = do
print (caudaSegura [] :: [Int])
print (caudaSegura [1::Int])
print (caudaSegura [1,2,3::Int])Executa o código para ver a saída e os gráficos.
Pista
Uma lista só tem dois construtores.
Mais uma pista
O resultado do caso x:xs é a cauda xs.
Solução
caudaSegura [] = []; caudaSegura (_:xs) = xs. As duas
equações cobrem as formas da lista sem comparar os elementos.
Confere a tua resposta:
- Trato [] explicitamente.
- Retiro a cabeça de uma lista não vazia.
- Conservo o tipo [a] -> [a].
Erros frequentes
Não uses head nem tail na equação do
caso vazio.