Tipos algébricos e recursão

Definir tipos com data, encaixe de padrões e recursão sobre listas e árvores.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Uma árvore binária pode estar vazia ou conter um valor e duas subárvores. Uma declaração data representa estas alternativas com construtores. Os padrões das funções desmontam cada alternativa e a recursão segue as subárvores. A página começa pela ausência como valor, define árvores e tipos, e termina com provas, substituição e conversão entre tipos recursivos.

Ausência como valor

Uma média sobre uma lista vazia não tem valor. Maybe a representa essa ausência com Nothing ou a presença com Just x:

segura :: Double -> Double -> Maybe Double
segura _ 0 = Nothing
segura x y = Just (x / y)

segura 5 0 dá Nothing. segura 5 2 dá Just 2.5. Quem recebe um Maybe tem de tratar os dois construtores com padrões ou case. A média que pode faltar mostra o mesmo contrato numa função sobre listas.

data e padrões

Uma árvore binária ou está vazia ou é um nó com um valor e duas subárvores. A definição é quase essa frase:

data Arvore a = Vazia | No a (Arvore a) (Arvore a)

Vazia é um construtor sem argumentos e No guarda um valor e duas árvores. Funções sobre a árvore têm uma equação por construtor:

emOrdem :: Arvore a -> [a]
emOrdem Vazia = []
emOrdem (No x esq dir) = emOrdem esq ++ [x] ++ emOrdem dir

O padrão (No x esq dir) desmonta o nó e dá nome às peças. A travessia em ordem lista a esquerda, depois o valor, depois a direita. O caso base Vazia devolve a lista vazia, e é ele que termina a recursão, como o [] nas listas da primeira página.

Pesquisa em árvores binárias

Nesta árvore de pesquisa, todos os valores da subárvore esquerda são menores que o valor do nó; todos os da direita são maiores ou iguais. A condição vale em cada nó, não apenas entre pais e filhos. inserir mantém esta invariância e procura usa-a para excluir uma subárvore:

inserir :: Ord a => a -> Arvore a -> Arvore a
inserir x Vazia = No x Vazia Vazia
inserir x (No y esq dir)
  | x < y     = No y (inserir x esq) dir
  | otherwise = No y esq (inserir x dir)

procura :: Ord a => a -> Arvore a -> Bool
procura _ Vazia = False
procura x (No y esq dir)
  | x == y    = True
  | x < y     = procura x esq
  | otherwise = procura x dir

Constrói a árvore inserindo 3, 1, 4, 1, 5 por ordem em Vazia: o 3 abre a raiz, o 1 vai para a esquerda e o 4 para a direita; o segundo 1 é igual ao primeiro, por isso cai no otherwise e vai para a direita dele, e o 5 vai para a direita do 4.

A travessia em ordem devolve [1,1,3,4,5], pois a invariância garante saída ordenada mesmo com o duplicado. A procura segue a mesma invariância: procura 4 compara com 3, desce à direita e encontra 4; procura 2 desce à esquerda de 3, compara com o primeiro 1, desce à sua direita, compara com o segundo 1 e chega a Vazia, dando False. Cada comparação escolhe uma subárvore, por isso o número de comparações depende da altura da árvore1.

árvore de pesquisa no navegador
data Arvore a = Vazia | No a (Arvore a) (Arvore a)

emOrdem :: Arvore a -> [a]
emOrdem Vazia = []
emOrdem (No x esq dir) = emOrdem esq ++ [x] ++ emOrdem dir

inserir :: Ord a => a -> Arvore a -> Arvore a
inserir x Vazia = No x Vazia Vazia
inserir x (No y esq dir)
  | x < y     = No y (inserir x esq) dir
  | otherwise = No y esq (inserir x dir)

procura :: Ord a => a -> Arvore a -> Bool
procura _ Vazia = False
procura x (No y esq dir)
  | x == y    = True
  | x < y     = procura x esq
  | otherwise = procura x dir

arv :: Arvore Int
arv = foldl (flip inserir) Vazia [3,1,4,1,5]

main :: IO ()
main = do
  print (emOrdem arv)
  print (procura 4 arv)
  print (procura 2 arv)
Resultado

Executa o código para ver a saída e os gráficos.

Quando a resposta pode faltar

A média ingénua das aprovações dividia por zero na lista vazia. O contrato que declara “valor ou ausência” é o mesmo Maybe da abertura:

mediaSegura :: [Double] -> Maybe Double
mediaSegura [] = Nothing
mediaSegura xs = Just (sum xs / fromIntegral (length xs))

Nothing é a ausência e Just embrulha o valor presente. O compilador pode avisar se falta um caso, mas ainda permite funções parciais; usar Maybe não obriga, por si só, a esse tratamento.

Construtores, sinónimos e recursão estrutural

type Ponto = (Double,Double) dá outro nome a um tipo existente. data Forma = Circulo Double | Retangulo Double Double cria um tipo novo com duas alternativas. Uma função area :: Forma -> Double precisa de considerar cada construtor: area (Circulo r) = pi*r*r e area (Retangulo a b) = a*b. A validade de dimensões positivas continua a ser uma condição separada.

O nome do tipo aparece nas assinaturas; o nome do construtor aparece nos valores e padrões. Em data Arvore a = Vazia | No a (Arvore a) (Arvore a), No tem tipo a -> Arvore a -> Arvore a -> Arvore a. A função construtora também pode ser parcialmente aplicada.

O esquema de recursão segue os componentes recursivos: para contar nós, o caso vazio dá zero e o nó dá 1 + tamanho esq + tamanho dir; para a altura, com vazio de altura zero, o nó dá 1 + max (altura esq) (altura dir). Numa folha, as duas subárvores vazias têm altura zero, dando altura um; indica a convenção, pois outra definição pode dar altura menos um à árvore vazia.

Uma árvore de expressões

data Expr = Valor Integer | Soma Expr Expr | Produto Expr Expr
  deriving (Eq, Show)

avaliar :: Expr -> Integer
avaliar (Valor n) = n
avaliar (Soma a b) = avaliar a + avaliar b
avaliar (Produto a b) = avaliar a * avaliar b

Soma (Valor 2) (Produto (Valor 3) (Valor 4)) representa 2+3*4. Avaliamos as folhas para 2, 3, 4, o produto para 12 e a soma para 14, porque a precedência já está na estrutura; avaliar não decide que operador tem prioridade.

deriving (Eq,Show) pede ao compilador implementações baseadas nos construtores e campos; num tipo parametrizado, comparar duas Arvore a requer também Eq a. Either erro valor é outro tipo algébrico que permite explicar por que o cálculo falhou.

Justificar uma lei por indução

Para provar length (map f xs) = length xs, considera listas finitas com comprimento representável em Int e os valores necessários à avaliação definidos. Usamos as equações de map e length.

Caso vazio: map f [] = [], por isso os dois comprimentos são 0.

Passo recursivo: supõe que a igualdade vale para a cauda xs. Então:

length (map f (x:xs))
= length (f x : map f xs)       pela equação de map
= 1 + length (map f xs)        pela equação de length
= 1 + length xs                pela hipótese sobre xs
= length (x:xs)                pela equação de length

A hipótese aplica-se à cauda menor, sem assumir a igualdade para a lista inteira em prova; length não precisa de avaliar f x, apenas de contar a célula construída.

Uma propriedade testada em exemplos reforça a confiança, mas não substitui esta prova para todas as listas finitas. A afirmação sobre length não pode ser avaliada numa lista infinita.

Substituir variáveis conservando a expressão

Avaliar uma expressão e construir outra expressão são tarefas diferentes. Nesta variante, acrescentamos folhas com nomes de variáveis:

data ExprVar = Const Integer | Var String
             | Add ExprVar ExprVar | Mult ExprVar ExprVar
  deriving (Eq, Show)

Um ambiente [(String,Integer)] liga nomes a valores. Substituir com [("x",4)] em Mult (Var "x") (Add (Var "z") (Const 2)) troca apenas a folha de x:

ParteResultado da substituição
Var "x"Const 4
Var "z"Var "z", porque não há ligação
Const 2Const 2
Nó Add ou MultMesmo construtor, com os dois filhos transformados

A resposta é Mult (Const 4) (Add (Var "z") (Const 2)). Não calculamos o produto nem inventamos zero para a variável ausente. lookup devolve Maybe Integer; Nothing conserva a variável e Just n constrói uma constante. Se o ambiente tiver um nome repetido, lookup usa a primeira ocorrência; decide esta política antes de escrever o resultado.

Converter uma árvore para outro tipo

Uma árvore binária não vazia pode ter folhas com valores ou ramos com dois filhos e um valor. Uma árvore rosa guarda uma lista de filhos:

data Bin a = Folha a | Ramo (Bin a) a (Bin a)
data Rosa a = Rosa a [Rosa a]

paraRosa :: Bin a -> Rosa a
paraRosa (Folha x) = Rosa x []
paraRosa (Ramo e x d) = Rosa x [paraRosa e, paraRosa d]

A folha torna-se um nó sem filhos. O ramo conserva o valor e coloca as duas conversões na ordem esquerda, direita. Não é uma travessia que achata a árvore: cada ramo continua a guardar os seus filhos. O tipo não exige Ord, Eq nem Num, porque a conversão não compara nem calcula valores.

Substituir folhas e converter árvores
data ExprVar = Const Integer | Var String
             | Add ExprVar ExprVar | Mult ExprVar ExprVar
  deriving (Eq, Show)

subst :: [(String,Integer)] -> ExprVar -> ExprVar
subst _ (Const n) = Const n
subst env (Var x) = case lookup x env of
  Nothing -> Var x
  Just n -> Const n
subst env (Add a b) = Add (subst env a) (subst env b)
subst env (Mult a b) = Mult (subst env a) (subst env b)

data Bin a = Folha a | Ramo (Bin a) a (Bin a)
  deriving (Eq, Show)
data Rosa a = Rosa a [Rosa a]
  deriving (Eq, Show)

paraRosa :: Bin a -> Rosa a
paraRosa (Folha x) = Rosa x []
paraRosa (Ramo e x d) = Rosa x [paraRosa e, paraRosa d]

main :: IO ()
main = do
  print (subst [("x",4)] (Mult (Var "x") (Add (Var "z") (Const 2))))
  print (subst [("x",4),("x",9)] (Var "x"))
  print (paraRosa (Ramo (Folha 'a') 'b' (Ramo (Folha 'c') 'd' (Folha 'e'))))
Resultado

Executa o código para ver a saída e os gráficos.

Nos dois algoritmos, cada chamada recursiva recebe uma subexpressão ou uma subárvore própria. Essa é a medida de progresso em entradas finitas. Para a substituição, com m entradas no ambiente e n nós na expressão, procurar cada variável com lookup pode exigir ordem n*m comparações no pior caso; a conversão visita cada nó uma vez.

Notas de rodapé

  1. O custo logarítmico vale quando a árvore está equilibrada: a altura cresce com o logaritmo do número de nós. Uma árvore equilibrada não precisa de ter metades exatamente iguais. Numa árvore degenerada em lista, a procura degrada-se para linear. Estruturas como Data.Set mantêm o equilíbrio; a função inserir desta página não o faz. Voltar

Exercícios

Declarar a convenção de altura

Com altura Vazia=0 e altura de um nó igual a 1+max das alturas dos filhos, qual a altura de uma cadeia com quatro nós?

Pista

Calcula a partir da folha.

Solução

As alturas no regresso são 1,2,3,4. A raiz tem altura 4.

Erros frequentes

Não contes arestas se o enunciado conta nós.

Voltar à explicação
A travessia está sempre ordenada?

A travessia em ordem de qualquer árvore binária dá uma lista ordenada?

A tua resposta

Pista

Desenha raiz 2 com filho esquerdo 5.

Solução

A lista [5,2] não está ordenada. É a invariância da árvore de pesquisa que justifica o resultado, não a forma binária.

  • Não; exige a invariância de pesquisa.. Uma árvore arbitrária pode ter um valor maior à esquerda.
  • Sim; visitar esquerda antes da raiz chega.. A ordem da visita não impõe a ordem dos valores.
  • Sim, se não houver repetidos.. Valores distintos podem continuar fora de ordem.

Erros frequentes

Não transportes uma propriedade de BST para todas as árvores.

Voltar à explicação
Uma prova sobre uma cauda menor

Prova por indução que length (filter p xs) <= length xs, para listas finitas de comprimento representável em Int e um predicado que termina.

Pista

Expande filter e length em x:xs. O filtro pode conservar ou excluir x.

Mais uma pista

Usa a hipótese length(filter p xs) <= length xs em cada alternativa.

Solução

No caso [] temos 0 ≤ 0. No passo x:xs, se p x é True, o comprimento filtrado é 1+length(filter p xs), que pela hipótese é no máximo 1+length xs. Se p x é False, fica length(filter p xs), no máximo length xs e portanto no máximo 1+length xs. Este último valor é length(x:xs).

Confere a tua resposta:

  • Verifico o caso [].
  • Enuncio a hipótese para xs.
  • Separo o passo nos casos p x=True e p x=False.

Erros frequentes

Não uses a afirmação pretendida sobre a lista inteira como hipótese.

Voltar à explicação
Inserir repetidos e seguir a pesquisa

Com Vazia, No e as funções inserir, emOrdem e procura da lição, insere 3,1,3,2 nesta ordem numa árvore vazia. Desenha a árvore, calcula a travessia em ordem e segue a procura de 4 até à resposta.

Pista
Aplica a guarda em cada nó, não apenas na raiz.
Solução

O primeiro 3 é a raiz. 1 fica à esquerda. O segundo 3 não satisfaz x < y e fica à direita da raiz. 2 segue à esquerda de 3 e à direita de 1:

    3
   / \
  1   3
   \
    2

A travessia dá [1,2,3,3]. Procurar 4 passa pela raiz 3, pelo 3 à direita e pela sua subárvore direita Vazia, dando False. A inserção conserva as duas ocorrências de 3.

Confere a tua resposta:

  • Desenho as inserções pela ordem dada.
  • Conservo o duplicado à direita do primeiro 3.
  • Obtenho a travessia e o percurso da pesquisa falhada.

Erros frequentes

Uma igualdade segue para a direita pela convenção desta função; não significa eliminar o duplicado.
Voltar à explicação
Substituir sem inventar valores

Com ExprVar da lição, define subst para substituir variáveis pelo ambiente, conservando as ausentes. Calcula a aplicação com ambiente [("x",7),("x",9)] e expressão Mult (Add (Var "x") (Const 2)) (Var "y"). O resultado é um número? Justifica a política para o nome repetido.

Solução

A função tem um caso por construtor:

data ExprVar = Const Integer | Var String
             | Add ExprVar ExprVar | Mult ExprVar ExprVar
  deriving (Eq, Show)

subst :: [(String,Integer)] -> ExprVar -> ExprVar
subst _ (Const n) = Const n
subst env (Var x) = case lookup x env of
  Nothing -> Var x
  Just n -> Const n
subst env (Add a b) = Add (subst env a) (subst env b)
subst env (Mult a b) = Mult (subst env a) (subst env b)

lookup "x" encontra a primeira ligação, 7. Não encontra y, que permanece uma variável. A resposta é Mult (Add (Const 7) (Const 2)) (Var "y"), uma expressão, não um número. Nem a soma 7+2 é calculada por subst: a tarefa conserva os construtores e transforma as folhas.

A recursão trata os dois filhos de cada operação. Uma versão que só trate Const deixa os restantes padrões sem resposta; uma que substitua uma variável ausente por zero muda o significado da expressão.

Confere a tua resposta:

  • Trato os quatro construtores de ExprVar.
  • Conservo y porque não existe no ambiente.
  • Uso a primeira ligação de x e conservo a estrutura.
Voltar à explicação
Conservar ramos ao mudar de representação

Usa Bin a e Rosa a da lição. Define paraRosa e converte Ramo (Folha 'a') 'b' (Ramo (Folha 'c') 'd' (Folha 'e')). Indica os filhos de b e d. São necessárias restrições Ord a ou Eq a? A função também pode devolver qualquer árvore rosa como conversão de uma binária?

Solução
data Bin a = Folha a | Ramo (Bin a) a (Bin a)
  deriving (Eq, Show)
data Rosa a = Rosa a [Rosa a]
  deriving (Eq, Show)

paraRosa :: Bin a -> Rosa a
paraRosa (Folha x) = Rosa x []
paraRosa (Ramo e x d) = Rosa x [paraRosa e, paraRosa d]

A resposta é Rosa 'b' [Rosa 'a' [], Rosa 'd' [Rosa 'c' [], Rosa 'e' []]]. Os filhos de b são a e d, nessa ordem; os de d são c e e. As folhas têm listas de filhos vazias. A função transporta valores sem os comparar, por isso não precisa de classes.

Esta imagem da conversão só contém nós com zero ou dois filhos. O tipo Rosa a permite também um ou três filhos, que nenhuma entrada deste tipo Bin a produz. Converter estruturas não implica que os dois tipos sejam isomorfos.

Confere a tua resposta:

  • Transformo Folha em Rosa sem filhos.
  • Conservo esquerda e direita nessa ordem.
  • Não acrescento restrições de classe que a função não usa.
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.