Playlists
Uma propriedade que se torna evidente em estruturas de dados (EDs) funcionais é que elas são sempre persistentes
Listas são onipresentes em linguages funcionais e são expressas naturalmente em cálculo-\(\lambda\). As operações que elas dão suporte são normalmente as mesmas daquelas oferecidas por pilhas (en: stacks)
nova :: Pilha a
Cria uma nova pilha vaziavazia :: Pilha a -> Bool
Verdadeiro se pilha vaziaempilha :: a -> Pilha a -> Pilha a
Empilha um valortopo :: Pilha a -> a
Recupera o valor no topo da pilhadesempilha :: Pilha a -> Pilha a
Desempilha um valor-- Implementação na unha!
data Pilha a = Vazia | Cons a (Pilha a)
nova :: Pilha a
nova = Vazia
vazia :: Pilha a -> Bool
vazia Vazia = True
vazia _ = False
empilha :: a -> Pilha a -> Pilha a
empilha = Cons
topo :: Pilha a -> a
topo Vazia = error "Pilha vazia"
topo (Cons v _) = v
desempilha :: Pilha a -> Pilha a
desempilha Vazia = error "Pilha vazia"
desempilha (Cons _ p) = p
Ou, se utilizarmos as listas presentes por padrão em Haskell, teríamos:
type Pilha' a = [a]
nova' :: Pilha' a
nova' = []
vazia' :: Pilha' a -> Bool
vazia' = null
empilha' :: a -> Pilha' a -> Pilha' a
empilha' = (:)
topo' :: Pilha' a -> a
topo' = head
desempilha' :: Pilha' a -> Pilha' a
desempilha' = tail
A partir de agora, seja uma lista ou uma pilha, utilizaremos a nomenclatura tradicional de listas.
Momento cultural
Concatenar tem a mesma raiz etimológica de catenária e ambas vêm
do latim catena: cadeia, corrente… Em inglês há inclusive o
verbo catenate equivalente ao nosso concatenar.
Falando em catenária, outra operação comum em listas é a
concatenação, normalmente representada pelo operador (++)
.
A concatenação de listas pode ser implementada facilmente em linguagens imperativas em tempo constante (\(O(1)\)).
Porém, essa implementação imperativa é destrutiva em seus
parâmetros. Após a execução de (++)
, nem xs
nem ys
podem ser
usadas novamente.
Em uma linguagem funcional não há a possibilidade de fazer operações destrutivas, então a saída é fazer uma implementação que copia células, mantendo os valores e trocando o valor do apontador para a próxima célula.
(++)
fica:(++) :: [a] -> [a] -> [a]
(++) [] ys = ys
(++) (x:xs) ys = x : (xs ++ ys)
A figura abaixo representa essa operação
Ganhamos persistência, mas agora o custo passou a ser \(O(n)\) onde
\(n\), é o comprimento de xs
.
Um caso parecido ocorre quando queremos fazer a atualização de um elemento com índice \(i\) da lista. Neste caso, precisamos copiar as células da lista ligada até o elemento desejado.
O código fica:
update :: [a] -> Int -> a -> [a]
update [] _ _ = error "Tentando atualizar lista vazia"
update (_:xs) 0 v = v:xs
update (x:xs) i v = x : update xs (i - 1) v
Graficamente ys = update xs 2 7
:
Apenas para reforçar: copiamos apenas o caminho até o elemento desejado.
Essa implementação se baseia fortemente no coletor de lixo (en: garbage collector). Implementar algo dessa maneira com controle de memória manual ficaria rapidamente complicadíssimo por conta dos compartilhamentos.
sufixos :: [a] -> [[a]]
que recebe uma
lista xs
e devolve uma lista com todos os sufixos da lista
xs
em ordem decrescente de comprimento. Por exemplo:
sufixos [1, 2, 3, 4] =
[[1,2,3,4],[2,3,4],[3,4],[4],[]]
Como fazer uma lista duplamente encadeada caso queiramos percorrê-la em ambas as direções?
-- esquerda valor direita
data Lista2 a = Vazia2 | Cons2 (Lista2 a) a (Lista2 a)
null2 :: Lista2 a -> Bool
null2 Vazia2 = True
null2 _ = False
head2 :: Lista2 a -> a
head2 Vazia2 = error "Lista vazia"
head2 (Cons2 _ x _) = x
tail2 :: Lista2 a -> Lista2 a
tail2 Vazia2 = Vazia2
tail2 (Cons2 _ _ d) = d
Parece bom! Mas e agora, como implementar a função cons2
?
cons2 :: a -> Lista2 a -> Lista2 a
cons2 = undefined -- ??
Aqui veremos o uso aplicado de Zippers, mas existem maneiras formais de se definir um zipper para uma estrutura de dados qualquer. Na verdade um zipper é definido como a derivada da ADT[^fn:1]. Mais informações também podem ser vistas na aula sobre ADTs.
Zipper é um idiom (do inglês: expressão idiomática) de linguagens funcionais muito utilizado para manipular estruturas de dados persistentes.
Zippers podem ser usados não apenas para percorrer estruturas de dados como também para auxiliar em sua modificação.
Mas afinal, o que é um zipper?
Zippers funcionam de uma maneira parecida com aquela pela qual uma formiga explora o mundo.
Como implementar tal abstração?
Simples! 2 listas!
type ListZipper a = ([a], [a])
toListZipper :: [a] -> ListZipper a
toListZipper xs = ([], xs)
fromListZipper :: ListZipper a -> [a]
fromListZipper (es, ds) = reverse es ++ ds
lzFoco :: ListZipper a -> Maybe a
lzFoco (_, []) = Nothing
lzFoco (_, x:_) = Just x
lzDir :: ListZipper a -> Maybe (ListZipper a)
lzDir (_, []) = Nothing
lzDir (es, d:ds) = Just (d:es, ds)
lzEsq :: ListZipper a -> Maybe (ListZipper a)
lzEsq ([], _) = Nothing
lzEsq (e:es, ds) = Just (es, e:ds)
Tudo muito bem e muito bonito, mas além de percorrer a lista como uso isso para modificá-la?
lzTrocaFoco :: a -> ListZipper a -> Maybe (ListZipper a)
lzTrocaFoco _ (_, []) = Nothing
lzTrocaFoco y (es, _:ds) = Just (es, y:ds)
Agora podemos definir a função que troca os cincos:
trocaCincos :: [Int] -> [Int]
trocaCincos [] = []
trocaCincos xs =
fromListZipper $ trocaZip $ toListZipper xs
where
trocaZip z@(_, []) = z
trocaZip z@(_, d:_) =
let nz = if d == 5
then fromJust $ lzTrocaFoco 42 z
else z in
maybe nz trocaZip (lzDir nz)
-- Mas pra que tudo isso?
trocaCincos' :: [Int] -> [Int]
trocaCincos' xs = [if x == 5 then 42 else x | x <- xs]
Listas são estruturas de dados relativamente simples. Por isso a manipulação de um elemento é fácil…
Pense agora em estruturas um pouco mais elaboradas, como árvores ou grafos.
O caminho por onde passamos é justamente o caminho armazenado no Zipper!!
Estude a função trocaCincos
e as funções que ela utiliza. Em
seguida reimplemente a função fromListZipper :: ListZipper a -> [a]
.
Você não pode utilizar as funções (++)
ou reverse
(nem
reimplementá-las!).
trocaCincos :: [Int] -> [Int]
trocaCincos [] = []
trocaCincos xs =
fromListZipper $ trocaZip $ toListZipper xs
where
trocaZip z@(_, []) = z
trocaZip z@(_, d:_) =
let nz = if d == 5
then fromJust $ lzTrocaFoco 42 z
else z in
maybe nz trocaZip (lzDir nz)
Experimente o código dessa seção no Repl.it abaixo:
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.