Expressões, avaliação e tipos em Haskell

Avaliar expressões por substituição, definir funções por equações e ler tipos predefinidos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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 e sinal no navegador
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)
Resultado

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:

TipoValoresExemplo
Intinteiros de precisão fixa42, -7
Integerinteiros de precisão arbitrária10^100
Floataproximações em precisão simples3.14
Doubleaproximações em precisão dupla3.14
BoollógicosTrue, False
Charcarateres'a'
Stringlistas 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.

Voltar à explicação
Um programa que compila pode falhar

Qual das expressões tem tipo mas falha quando o resultado é pedido?

A tua resposta

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.

Voltar à explicação
Cobrir a lista vazia

Define caudaSegura com padrões. Os testes devem produzir [] , [] e [2,3].

Completa e compara com os resultados pedidos
caudaSegura :: [a] -> [a]
caudaSegura xs = undefined
main :: IO ()
main = do
  print (caudaSegura [] :: [Int])
  print (caudaSegura [1::Int])
  print (caudaSegura [1,2,3::Int])
Resultado

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.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.