Playlists
Padrões de compartilhamento mais interessantes podem aparecer quando temos mais de um campo por nó. Árvores binárias apresentam esses padrões.
Árvores binárias de busca são árvores binárias cujos elementos internos são organizados em ordem simétrica .
-- Podemos representar uma árvore binária como
data Arv a = ArvVazia | No (Arv a) a (Arv a)
O código abaixo define uma typeclass para um conjunto:
class Conjunto t where
membro :: Ord a => a -> t a -> Bool
insere :: Ord a => a -> t a -> t a
Uma typeclass (classe de tipos) é semelhante a uma interface que
define algum comportamento. A nossa typeclass Conjunto
define dois
métodos: membro
e insere
. Qualquer tipo x
que deseje
“participar” desta typeclass precisa fornecer implementações de
funções para estes dois métodos. Quando isto é feito dizemos que declaramos uma
instância da typeclass para o tipo x
.
Não caia na confusão de achar que os termos comuns em orientação a objeto classes, instâncias e métodos têm qualquer coisa a ver com os termos homônimos em Haskell! Eles não tem relação!
-- Podemos representar uma árvore binária como
data Arv a = ArvVazia | No (Arv a) a (Arv a)
E para tornar nossa árvore um conjunto podemos fazer:
instance Conjunto Arv where
membro _ ArvVazia = False
membro x (No e v d)
| x < v = membro x e
| x > v = membro x d
| otherwise = True
insere x ArvVazia = No ArvVazia x ArvVazia
insere x t@(No e v d)
| x < v = No (insere x e) v d
| x > v = No e v (insere x d)
| otherwise = t
A execução da função membro
é trivial. Vamos verificar com mais
cuidado a execução de insere
com o elemento e
:
Antes
Depois
Os três percursos tradicionais de árvores, pré/em/pós-ordem podem ser aplicados de maneira trivial.
Ou em código que imprime o conteúdo dos nós:
preOrdem, inOrdem, posOrdem :: Show a => Arv a -> IO ()
preOrdem ArvVazia = return ()
preOrdem (No e x d) = do
print x
preOrdem e
preOrdem d
inOrdem ArvVazia = return ()
inOrdem (No e x d) = do
inOrdem e
print x
inOrdem d
posOrdem ArvVazia = return ()
posOrdem (No e x d) = do
posOrdem e
posOrdem d
print x
Mas e se quisermos uma busca em largura? As implementações tradicionais para percorrer uma árvore lançam mão de uma fila.
Vamos usar a implementação de fila fuleira® baseada em listas.
-- Fila "fuleira"®
type Queue a = [a]
-- O(1)
empty :: Queue a
empty = []
-- O(1)
isEmpty :: Queue a -> Bool
isEmpty = null
Operações na fila fuleira®:
-- O(1)
enq :: Queue a -> a -> Queue a
enq = flip (:)
-- O(n)
deq :: Queue a -> (a, Queue a)
deq xs = (last xs, init xs)
larguraQ :: Show a => Arv a -> IO ()
larguraQ arv =
larguraFila $ enq empty arv
where
larguraFila q =
if isEmpty q then return ()
else case deq q of
(ArvVazia, q') -> larguraFila q'
(No e x d, q') -> do
print x
larguraFila $ enq (enq q' e) d
Com as estruturas de dados que temos até agora (apenas listas), apesar de possível, ainda não é trivial implementar uma estrutura de dados para fila que seja eficiente.
Já é possível, contudo, implementar uma ED com complexidade amortizada \(O(1)\) (falaremos mais sobre isso mais adiante no curso).
Uma outra abordagem (sem usar filas) é utilizar um algoritmo baseado em níveis:
larguraNivel :: Show a => Arv a -> IO ()
larguraNivel arv =
larguraNivel' [arv]
where
printArv ArvVazia = return ()
printArv (No _ x _) = print x
filhos ArvVazia = []
filhos (No e _ d) = [e, d]
larguraNivel' [] = return ()
larguraNivel' lvl = do
mapM_ printArv lvl
larguraNivel' $ concatMap filhos lvl
Vamos considerar agora um problema um pouco diferente: numeração dos nós em largura (en: breadth-first numbering, BFN).
Considere que queremos uma função:
bfn :: Arv a -> Arv (a, Int)
onde o segundo elemento da tupla é a ordem em que este elemento foi visitado pela varredura.
Se fosse para fazer os casos tradicionais (a.k.a. fáceis) pré/in/pós-ordem…
numeraPreOrdem :: Arv a -> Arv (a, Int)
numeraPreOrdem arv =
snd $ numeraPreOrdem' 1 arv
where
numeraPreOrdem' i ArvVazia = (i, ArvVazia)
numeraPreOrdem' i (No e x d) =
(i3, No e' (x, i) d')
where
(i2, e') = numeraPreOrdem' (i + 1) e
(i3, d') = numeraPreOrdem' i2 d
numeraInOrdem :: Arv a -> Arv (a, Int)
numeraInOrdem arv =
snd $ numeraInOrdem' 1 arv
where
numeraInOrdem' i ArvVazia = (i, ArvVazia)
numeraInOrdem' i (No e x d) =
(i3, No e' (x, i2) d')
where
(i2, e') = numeraInOrdem' i e
(i3, d') = numeraInOrdem' (i2 + 1) d
numeraPosOrdem :: Arv a -> Arv (a, Int)
numeraPosOrdem arv =
snd $ numeraPosOrdem' 1 arv
where
numeraPosOrdem' i ArvVazia = (i, ArvVazia)
numeraPosOrdem' i (No e x d) =
(i3 + 1, No e' (x, i3) d')
where
(i2, e') = numeraPosOrdem' i e
(i3, d') = numeraPosOrdem' i2 d
Já a versão em largura é um pouco mais trabalhosa…
bfnFila t =
fst $ deq $ bfn' 1 (enq empty t)
where
bfn' i inQ
| isEmpty inQ = empty
| otherwise =
case deq inQ of
(ArvVazia, inQ1) -> enq (bfn' i inQ1) ArvVazia
(No e x d, inQ1) ->
let
inQ2 = enq (enq inQ1 e) d
outQ0 = bfn' (i + 1) inQ2
(d', outQ1) = deq outQ0
(e', outQ2) = deq outQ1 in
enq outQ2 (No e' (x, i) d')
Apesar de, a princípio, esta versão parecer complicada ela é bem eficiente.
bfn2 arv = head $ bfn' 1 [arv]
where
filhos ArvVazia = []
filhos (No a _ b) = [a, b]
recons _ [] [] = []
recons i (ArvVazia:ts) cs= ArvVazia : recons i ts cs
recons i ~(No _ x _ : ts) ~(a : b : cs) =
No a (x, i) b : recons (i + 1) ts cs
bfn' _ [] = []
bfn' i lvl =
let
proxNivel = concatMap filhos lvl
j = i + length proxNivel `div` 2
proxNivelNum = bfn' j proxNivel in
recons i lvl proxNivelNum
Além de pavorosa, essa versão não é nem um pouco eficiente. Ela varre cada nível 3 vezes!
Veremos em seguida como fazer isto de maneira mais eficiente e simples explorando a avaliação preguiçosa disponível em praticamente todas as linguagens de programação funcional.
Tal qual fizemos no exemplo de Fibonacci e dos Números Primos, imagine que magicamente caia no seu colo uma lista com os índices dos primeiros elementos de cada nível.
Usando essa lista “mágica”, a função abaixo numera os nós em largura:
-- Versão lazy por Jones e Gibbons
bfnJG' :: ([Int], Arv a) -> ([Int], Arv (a, Int))
bfnJG' (ks, ArvVazia) = (ks, ArvVazia)
bfnJG' (k : ks0, No a x b) =
(k + 1 : ks2, No a' (x, k) b')
where
(ks1, a') = bfnJG' (ks0, a)
(ks2, b') = bfnJG' (ks1, b)
Compare a saída de bfn'
com a sua entrada. O que você vê?
💡 💡 Vamos plugar a saída na entrada! 💡 💡
Precisamos apenas adicionar o primeiro nível:
bfnJG t = t'
where (ks, t') = bfnJG' (1 : ks, t)
Uma descrição mais pormenorizada da solução proposta por Jones e Gibbons pode ser encontrada em seu paper.
Para realmente entender
Para realmente entender o que está ocorrendo aqui,
explique porque trocar a implementação de bfnJG
de:
bfnJG t = t'
where (ks, t') = bfnJG' (1 : ks, t)
para:
bfnJG2 t =
snd $ bfnJG' (1 : ks, t)
where
ks = tail (1 : ks)
não funciona.
A depender da versão do GHC, você pode receber um erro: <<loop>>
.
ao executar bfnJG2
.1
Experimente o código dessa seção no Repl.it abaixo:
Diferentemente do caso de listas, zippers para árvores não são unidimensionais.
Para percorrer uma árvore podemos:
Assim, nosso zipper terá as seguintes opções de deslocamento:
esq
→ caminha em direção às folhas pelo filho esquerdodir
→ caminha em direção às folhas pelo filho direitocima
→ caminha em direção à raizAssim como o zipper de listas, o nosso zipper de árvores precisará guardar pelo menos o foco e o caminho percorrido.
-- Podemos representar uma árvore binária como
data Arv a = ArvVazia | No (Arv a) a (Arv a)
type ZipperArv a =
(Arv a, [Either (a, Arv a) (a, Arv a)])
toArvZipper :: Arv a -> ZipperArv a
toArvZipper arv = (arv, [])
fromArvZipper :: ZipperArv a -> Arv a
fromArvZipper (arv, []) = arv
fromArvZipper z = fromJust $ fromArvZipper <$> arvCima z
arvFoco :: ZipperArv a -> Maybe a
arvFoco (ArvVazia, _) = Nothing
arvFoco (No _ x _, _) = Just x
arvTrocaFoco :: a -> ZipperArv a -> Maybe (ZipperArv a)
arvTrocaFoco _ (ArvVazia, _) = Nothing
arvTrocaFoco x (No a _ b, rastro) =
Just (No a x b, rastro)
arvDir :: ZipperArv a -> Maybe (ZipperArv a)
arvDir (ArvVazia, _) = Nothing
arvDir (No e x d, rastro) =
Just (d, Right (x, e):rastro)
arvEsq :: ZipperArv a -> Maybe (ZipperArv a)
arvEsq (ArvVazia, _) = Nothing
arvEsq (No e x d, rastro) =
Just (e, Left (x, d):rastro)
arvCima :: ZipperArv a -> Maybe (ZipperArv a)
arvCima (_, []) = Nothing
arvCima (arv, Left (x, d):rastro) =
Just (No arv x d, rastro)
arvCima (arv, Right (x, e):rastro) =
Just (No e x arv, rastro)
Maybe
Note que como no caso do zipper para listas, todas as operações
que podem falhar devolvem um Maybe a
.
Isto torna as nossas funções totais evitando, em tempo de compilação, uma série de erros que normalmente ocorreriam apenas em tempo de execução.
Uma função é total se ela for definida para todos os valores possíveis do seu tipo de entrada.
Uma função é parcial se existem algumas entradas para as quais
ela não tem valor de saída definido.
> head []
*** Exception: Prelude.head: empty list
Tal qual fizemos com as listas, vamos trocar um valor por outro em nossa BST na unha e em seguida usando zippers!
trocaArv :: Ord a => a -> a -> Arv a -> Arv a
trocaArv _ _ ArvVazia = ArvVazia
trocaArv velho novo (No e x d)
| x < velho = No e x $ trocaArv velho novo d
| x > velho = No (trocaArv velho novo e) x d
| otherwise = No e novo d
trocaArvZ :: Ord a => a -> a -> Arv a -> Arv a
trocaArvZ velho novo arv =
fromArvZipper $ trocaArvZ' $ toArvZipper arv
where
trocaArvZ' z@(ArvVazia, _) = z
trocaArvZ' z
| f < velho = go arvDir
| f > velho = go arvEsq
| otherwise = fromJust $ arvTrocaFoco novo z
where
f = fromJust $ arvFoco z
go d = maybe z trocaArvZ' (d z)
Zippers foram propostos originalmente por Gérard Huet em 1997.
Certamente muitos que trabalharam com árvores durante os mais de 50 anos de linguagens funcionais fizeram buscas em largura.
data RoseTree a = EmptyRose | RoseTree a [RoseTree a]
arv = RoseTree 42 [EmptyRose]
data RoseTree a = EmptyRose | RoseTree a [RoseTree a]
RoseTree
que verifica, em tempo de
execução, que não estamos recebendo árvores vazias para incluir
na lista. 🙄Talvez um exemplo seja ser mais esclarecedor…
data FormData a = FormData String
O exemplo parece estranho… Porque um tipo a
se ele não é usado?
De fato é possível usar o tipo FormData
da seguinte maneira:
changeType :: FormData a -> FormData b
changeType (FormData str) = FormData str
Que troca o tipo simplesmente chamando o mesmo construtor dos dois lados da equação!
data Validated
data Unvalidated
-- Se escondermos os construtores dos usuários (basta
-- não exportá-los) essa função é a única que pode
-- ser usada pelos usuários. Ou seja, o usuário só
-- pode criar FormData que não são validados.
formData :: String -> FormData Unvalidated
formData str = FormData str
-- Podemos criar uma função que valida os dados
validate :: FormData Unvalidated -> Maybe (FormData Validated)
validate (FormData str) = ...
-- E assim GARANTIR que funções consumam apenas dados
-- que foram validados!
useData :: FormData Validated -> IO ()
useData (FormData str) = ...
data FormData a = FormData String
FormData Validated
e FormData Unvalidated
.FormData Int
.
formData
, apenas
a nossa própria biblioteca conseguiria tal feito. Mas nem isso
quero deixar em aberto!Para resolver, podemos fazer6:
data ValidationStatus = Validated | Unvalidated
data FormData (status :: ValidationStatus) where
FormData :: String -> FormData status
formData :: String -> FormData Unvalidated
formData str = FormData str
validate :: FormData Unvalidated -> Maybe (FormData Validated)
validate (FormData str) = ..
Ah sim! Estávamos falando de roseiras!
Utilizando tipos fantasmas, o código pode ser melhorado:
data Emptiness = Empty | NonEmpty
data RoseTree (e :: Emptiness) a where
EmptyRose :: RoseTree Empty a
RoseTree :: a -> [RoseTree NonEmpty a] -> RoseTree NonEmpty a
Aqui vamos apenas arranhar a superfície do que é possível fazer com type-safe programming e dependent types. Apesar de haver várias propostas em andamento para avançar a linguagem Haskell nesta direção, ela é atualmente incompleta. Caso queira explorar além do que falaremos por aqui, procure pelas linguagens: Idris, Agda e Coq.
data RoseTreeZipper a = RoseTreeZipper
a -- Valor do foco
(ListZipper (RoseTree NonEmpty a)) -- Subzipper da lista de filhos
[(a, ListZipper (RoseTree NonEmpty a))] -- Rastro
Vamos comparar7 com o zipper de árvores e listas:
type ZipperArv a =
(Arv a, [Either (a, Arv a) (a, Arv a)])
toRoseZipper :: RoseTree e a -> Maybe (RoseTreeZipper a)
toRoseZipper EmptyRose = Nothing
toRoseZipper (RoseTree v bs) =
Just $ RoseTreeZipper v (toListZipper bs) []
roseFoco :: RoseTreeZipper a -> a
roseFoco (RoseTreeZipper v _ _) = v
roseTrocaFoco :: RoseTreeZipper a-> a-> RoseTreeZipper a
roseTrocaFoco (RoseTreeZipper _ lz ps) x =
RoseTreeZipper x lz ps
roseDir :: RoseTreeZipper a -> Maybe (RoseTreeZipper a)
roseDir (RoseTreeZipper v lz ps) = do
lz' <- lzDir lz
Just $ RoseTreeZipper v lz' ps
roseEsq :: RoseTreeZipper a -> Maybe (RoseTreeZipper a)
roseEsq (RoseTreeZipper v lz ps) = do
lz' <- lzEsq lz
Just $ RoseTreeZipper v lz' ps
roseBaixo:: RoseTreeZipper a -> Maybe (RoseTreeZipper a)
roseBaixo (RoseTreeZipper v lz ps) = do
(RoseTree v' bs') <- lzFoco lz
Just $ RoseTreeZipper v' (toListZipper bs') ((v, lz) : ps)
roseCima :: RoseTreeZipper a -> Maybe (RoseTreeZipper a)
roseCima (RoseTreeZipper _ _ []) = Nothing
roseCima (RoseTreeZipper _ _ ((v',lz'):ps)) =
Just $ RoseTreeZipper v' lz' ps
A combinação das implementações das funções trocaFoco
e
roseCima
, tal como está, descarta eventuais alterações feitas no
valor em foco. Corrija a implementação.
data RoseTreeZipper a = RoseTreeZipper
a -- Valor do foco
(ListZipper (RoseTree NonEmpty a)) -- Subzipper da lista de filhos
[(a, ListZipper (RoseTree NonEmpty a))] -- Rastro
roseTrocaFoco :: RoseTreeZipper a-> a-> RoseTreeZipper a
roseTrocaFoco (RoseTreeZipper _ lz ps) x =
RoseTreeZipper x lz ps
roseCima :: RoseTreeZipper a -> Maybe (RoseTreeZipper a)
roseCima (RoseTreeZipper _ _ []) = Nothing
roseCima (RoseTreeZipper _ _ ((v',lz'):ps)) =
Just $ RoseTreeZipper v' lz' ps
Supostamente pois a impressora apenas era capaz de imprimir vermelho e preto…
Uma árvore rubro-negra é uma árvore de busca binária, logo segue todas as regras:
Além disto, cada nó de uma árvore rubro negra tem as seguintes características:
Exemplo 1.
É uma árvore rubro-negra?
Não! Viola a Regra 1 (A raiz é sempre preta).
Exemplo 2.
É uma árvore rubro-negra?
Não! Viola a Regra 4 (Para cada nó \(p\), todos os caminhos desde \(p\) até as folhas contêm o mesmo número de nós pretos).
Exemplo 3.
É uma árvore rubro-negra?
Sim!
Exemplo 4.
É uma árvore rubro-negra?
Não! Viola a Regra 2 (Nenhum nó vermelho tem filhos vermelhos).
Por simplicidade, no restante das figuras vamos representar as árvores omitindo as folhas nulas (pretas). Uma árvore válida seria então:
Restringindo a maneira que os nós podem ser coloridos do caminho da raiz até qualquer uma das suas folhas, as árvores rubro-negras garantem:
A altura negra de um nó \(n\) é definida como o número de nós pretos (sem incluir o próprio n) visitados em qualquer caminho de n até as folhas.
A altura negra do nó n é denotada por \(H_p(n)\).
Pela Regra 4, \(H_p(n)\) é bem definida para todos os nós da árvore.
Lema 1: A altura máxima de uma árvore rubro-negra com \(n\) nós internos é de \(2\text{lg}(n+1)\).
Corolário: As operações de Busca, Mínimo, Máximo, Sucessor e Predecessor podem ser efetuadas em tempo \(O(\text{lg}(n))\).
Busca – A busca que estamos acostumados funciona sem modificações.
Inserção e Remoção – Se feitas sem qualquer cuidado, apesar de manter as propriedades de árvores binárias de busca, podem ferir as propriedades rubro-negras.
Veja animação de operações em: http://tommikaikkonen.github.io/rbtree
Primeiro definimos as cores:
data RBColor = R | B
E em seguida, a árvore:
data RBTree a where
RBEmpty :: Ord a => RBTree a
RBT :: Ord a => RBColor -> RBTree a -> a -> RBTree a -> RBTree a
data RBTree a where
RBEmpty :: Ord a => RBTree a
RBT :: Ord a => RBColor -> RBTree a -> a -> RBTree a -> RBTree a
empty :: Ord a => RBTree a
empty = RBEmpty
singleton :: Ord a => a -> RBTree a
singleton x = RBT B empty x empty
null :: RBTree a -> Bool
null RBEmpty = True
null _ = False
head :: RBTree p -> p
head (RBT _ _ x _) = x
head _ = error "head: empty Red Black Tree"
data RBTree a where
RBEmpty :: Ord a => RBTree a
RBT :: Ord a => RBColor -> RBTree a -> a -> RBTree a -> RBTree a
elem :: a -> RBTree a -> Bool
elem _ RBEmpty = False
elem x (RBT _ l y r)
| x < y = elem x l
| x > y = elem x r
| otherwise = True
-- Percurso in-ordem
toList :: RBTree a -> [a]
toList RBEmpty = []
toList (RBT _ l v r) =
toList l ++ v : toList r
Quebra a Regra 0 – Todo nó deve ser vermelho ou preto.
Primeiramente, criamos uma função que cria a árvore alvo após o balanceamento:
buildRed :: Ord a => RBTree a -> a -> RBTree a -> a -> RBTree a -> a -> RBTree a -> RBTree a
buildRed a x b y c z d =
RBT R (RBT B a x b) y (RBT B c z d)
Agora basta descrever as transições dos 4 casos:
balance :: Ord a => RBColor -> RBTree a -> a -> RBTree a -> RBTree a
balance B (RBT R (RBT R a x b) y c) z d = -- Caso 1
buildRed a x b y c z d
balance B (RBT R a x (RBT R b y c)) z d = -- Caso 2
buildRed a x b y c z d
balance B a x (RBT R b y (RBT R c z d)) = -- Caso 3
buildRed a x b y c z d
balance B a x (RBT R (RBT R b y c) z d) = -- Caso 4
buildRed a x b y c z d
balance color a x b =
RBT color a x b
balance
garantem que as propriedades da árvore
serão mantidasinsert :: Ord a => a -> RBTree a -> RBTree a
insert x t = makeBlack $ ins t
where
ins RBEmpty = singleton x
ins t2@(RBT color l y r)
| x < y = balance color (ins l) y r
| x > y = balance color l y (ins r)
| otherwise = t2
makeBlack ~(RBT _ a y b) = RBT B a y b
Sobre a implementação
⋅ Mesmo sem otimizações, esta é uma das implementações funcionais
mais rápidas.
⋅ Os exercícios para casa tratam de possíveis otimizações que
fazem essa implementação voar!
Comparação com a versão imperativa
⋅ Se comparada à implementação imperativa ([CLRS], [SW]), a
implementação funcional é bem mais simples pois usa algumas
transformações um pouco diferentes.
⋅ Normalmente implementações imperativas dividem os 4 casos
apresentados aqui em 8 casos de acordo com a cor do irmão do nó
vermelho com filho vermelho.
⋅ Saber a cor do “tio” do nó vermelho permite, em alguns casos,
utilizar menos atribuições ou terminar o rebalanceamento sem
precisar propagá-lo até a raiz.
⋅ Tais otimizações em uma implementação persistente são
irrelevantes! Precisamos de qualquer forma copiar todo o caminho
até a raiz, então não há razão de utilizar as transformações mais
complicadas!
A implementação da inserção que apresentamos tem um erro grave! Você é capaz de apontá-lo?
insert :: Ord a => a -> RBTree a -> RBTree a
insert x t = makeBlack $ ins t
where
ins RBEmpty = singleton x
ins t2@(RBT color l y r)
| x < y = balance color (ins l) y r
| x > y = balance color l y (ins r)
| otherwise = t2
makeBlack ~(RBT _ a y b) = RBT B a y b
insert :: Ord a => a -> RBTree a -> RBTree a
insert x t = makeBlack $ ins t
where
ins RBEmpty = RBT R empty x empty
ins t2@(RBT color l y r)
| x < y = balance color (ins l) y r
| x > y = balance color l y (ins r)
| otherwise = t2
makeBlack ~(RBT _ a y b) = RBT B a y b
Quais outros erros poderiam estar escondidos na nossa implementação? O compilador não poderia nos ajudar a detectá-los?
A versão de árvore rubro-negra que apresentamos aqui é devida a Chris Okasaki e foi apresentada em 1999. Antes disso as implementações funcionais eram uma adaptação forçada das implementações imperativas.
Aqueles mais atentos vão perceber que houve a omissão de um tópico essencial: remoção funcional de nós.
Experimente o código dessa seção no Repl.it abaixo:
Assim como fizemos com as Roseiras, vamos melhorar o nosso código para que erros como o que cometemos (e alguns outros) não sejam possíveis.
Para isto vamos precisar relembrar um pouco de matemática.
Indicaremos por \(\sigma(n)\) o sucessor de \(n\) e, como usual, \(0\) para denotar o valor zero.
Os cinco axiomas são:
n
é codificado como a chamada de uma função n
vezes:ZERO = \f x -> x
UM = \f x -> f x
DOIS = \f x -> f (f x)
TRES = \f x -> f (f (f x))
-- Inteiros de Peano
data Peano = Zero | Succ Peano
E os números naturais passam a ser…
type One = Succ Zero
type Two = Succ One
type Three = Succ Two
...
… e nossa implementação atual:
data RBColor = R | B
data RBTree a where
RBEmpty :: Ord a => RBTree a
RBT :: Ord a => RBColor -> RBTree a -> a -> RBTree a -> RBTree a
data Color = R | B
type Black = Node B -- Apenas um atalho
type Red = Node R
data Node (c :: Color) a where -- regra 0
Null :: Ord a => Black a -- regra 3
RBT :: Ord a => c -> Node c0 a -> a -> Node c1 a -> Node c a
data Color = R | B
type Black = Node B
type Red = Node R
data RBTree a = Black a -- regra 1
data Node (c :: Color) a where -- regra 0
Null :: Ord a => Black a -- regra 3
RBT :: Ord a => c -> Node c0 a -> a -> Node c1 a -> Node c a
data Color = R | B
type Black = Node B
type Red = Node R
data RBTree a = Black a -- regra 1
data Node (c :: Color) a where -- regra 0
Null :: Ord a => Black a -- regra 3
Black::Ord a => Node c0 a -> a -> Node c1 a -> Black a
-- regra 2
Red :: Ord a => Black a -> a -> Black a -> Red a
data Peano = Zero | Succ Peano
type One = Succ Zero
data RBTree a = forall n. T (Black n a) -- regra 1
data Node (c :: Color) (n :: Peano) a where -- regra 0
Null :: Ord a => Black One a -- regra 3, 4
Black :: Ord a => Node c0 n a -> a -> Node c1 n a -> Black (Succ n) a -- regra 4
-- regra 2 e 4
Red :: Ord a => Black n a -> a -> Black n a -> Red n a
#SQN
A busca precisa apenas de algumas pequenas alterações para funcionar na nova ED:
elem :: Ord a => a -> RBTree a -> Bool
elem x (T node) = elemNode x node
elemNode :: a -> Node c n a -> Bool
elemNode x node =
case node of
Null -> False
(Black l y r) -> elem' x l y r
(Red l y r) -> elem' x l y r
where
elem' e l y r
| e < y = elemNode e l
| e > y = elemNode e r
| otherwise = True
A estratégia que utilizamos antes de:
… não funciona mais!
Os tipos proibem que criemos uma árvore inválida mesmo que temporariamente!
Precisamos montar uma árvore válida diretamente.
A ideia da implementação se baseia na seguinte observação: a inserção de um novo nó vermelho abaixo de…
Assim, dividimos a implementação da inserção em cores!
Violation
data Violation (n :: Peano) a where
Case14 -- Nós vermelhos à esq. esq.
:: Ord a => -- Caso 1 Caso 4
a -- Red y Red z
-> a -- Red x Red y
-> Black n a -- a b
-> Black n a -- b c
-> Black n a -- c d
-> Violation n a
Case23 -- Nós vermelhos à dir. dir.
:: Ord a => -- Caso 2 Caso 3
a -- Red x Red y
-> a -- Red y Red z
-> Black n a -- a b
-> Black n a -- b c
-> Black n a -- c d
-> Violation n a
-- insere o elemento x0 na árvore de raiz vermelha n
-- Fica sob responsabilidade do pai a correção de
-- eventuais violações.
insR :: a -> Red n a -> Either (Violation n a) (Red n a)
insR x0 n@(Red l0 y0 r0) -- l0 e r0 são pretos
| x0 < y0 =
case insB x0 l0 of
(Left black) -> mkRed black y0 r0
(Right (Red a x b)) -> Left $ Case14 y0 x a b r0
| x0 > y0 =
case insB x0 r0 of
(Left black) -> mkRed l0 y0 black
(Right (Red b y c)) -> Left $ Case23 y0 y l0 b c
| otherwise = Right n
where
mkRed a v c = Right $ Red a v c
-- insere o elemento x0 na árvore de raiz negra e
-- devolve um novo no pode ser vermelho ou negro
insB :: a -> Black n a -> Either (Black n a) (Red n a)
insB x0 Null = Right $ Red Null x0 Null
insB x0 n@(Black l0 y0 r0)
| x0 < y0 =
case l0 of
Null -> eitherInsBL x0 Null y0 r0
black@Black{} -> eitherInsBL x0 black y0 r0
red@Red{} ->
case insR x0 red of
(Left (Case14 y x a b c)) -> -- Caso 1
balance a x b y c y0 r0
(Left (Case23 x y a b c)) -> -- Caso 2
balance a x b y c y0 r0
(Right r) ->
mkBlack y0 r r0
-- O caso de x0 > y0 é semelhante, porém pode causar
-- as violações 3 ou 4
| x0 > y0 =
case r0 of
Null -> eitherInsBR x0 l0 y0 Null
black@Black{} -> eitherInsBR x0 l0 y0 black
red@Red{} ->
case insR x0 red of
(Left (Case14 z y b c d)) -> -- Caso 4
balance l0 y0 b y c z d
(Left (Case23 y z b c d)) -> -- Caso 3
balance l0 y0 b y c z d
(Right r) ->
mkBlack y0 l0 r
-- Neste caso o valor já estava na árvore
| otherwise = Left n
where
mkBlack x a b = Left $ Black a x b
eitherInsBR x l y r = either (mkBlack y l) (mkBlack y l) (insB x r)
eitherInsBL x l y r = either (flip (mkBlack y) r) (flip (mkBlack y) r) (insB x l)
balance a x b y c z d = Right $ Red (Black a x b) y (Black c z d)
E finalmente a inserção na árvore propriamente dita
-- insere o elemento x na árvore t
insert :: a -> RBTree a -> RBTree a
insert x (T node) =
-- pela regra 1 (e o tipo de RBTree) sabemos que node
-- é preto, então insB type-checks
case insB x node of
(Left b) -> T b
(Right (Red l v r)) -> T $ Black l v r
Quase!
GADTs
e DataKinds
para garantir as cores.Violation
como o que fizemos aqui para melhor organização e entendimento do
código é minha jaboticaba.Experimente o código dessa seção no Repl.it abaixo:
Huet, Gérard. “The zipper.” Journal of functional programming 7.5 (1997): 549-554.
Meertens, Lambert. “First steps towards the theory of rose trees.” CWI, Amsterdam (1988).
Eisenberg, Richard A. “Dependent types in haskell: Theory and practice.”. PhD Thesis, University of Pensylvania 2016.
Estes slides foram preparados para os cursos de Paradigmas de Programação e Desenvolvimento Orientado a Tipos na UFABC.
Este material pode ser usado livremente desde que sejam mantidos, além deste aviso, os créditos aos autores e instituições.
Para saber mais sobre como o GHC detecta estes tipos de problemas verifique a página https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/rts/storage/heap-objects, em especial a parte relacionada a black holes. ↩︎
Veremos mais adiante como a avaliação preguiçosa nos permite trabalhar com EDs infinitas. ↩︎
Esse código exige o uso
das extensões DataKinds, KindSignatures, GADTs
↩︎
Essa implementação não é minimal, mas facilita o entendimento. ↩︎
Essa
implementação usa a extensão DataKinds
↩︎