Professional Documents
Culture Documents
5 Data Handling With Lists - The Joy of Kotlin
5 Data Handling With Lists - The Joy of Kotlin
Nissocapítulo
Este capítulo aborda estruturas de dados e como criar sua própria implementação
da lista encadeada individualmente. Kotlin tem suas próprias listas, tanto mutá-
veis quanto imutáveis. Mas a lista imutável do Kotlin não é realmente imutável e
não implementa o compartilhamento de dados, tornando operações como adicio-
nar e remover elementos menos eficientes. oA lista imutável que você desenvol-
verá neste capítulo é muito mais eficiente para operações de pilha e é imutável.
IMPORTANTE O que você aprenderá neste capítulo não é específico para listas,
mas é compartilhado por muitas outras estruturas de dados (que podem não ser
coleções).
As listas podem ser classificadas com base em vários aspectos diferentes, in-
cluindo o seguinte:
Acesso —Você pode acessar algumas listas apenas de uma extremidade e ou-
tras de ambas as extremidades. As listas podem ser escritas de um lado e lidas
do outro lado. Finalmente, com algumas listas, você pode acessar qualquer ele-
mento usando sua posição na lista, que também é chamada deíndice .
Tipo de ordenação — Em algumas listas, os elementos são lidos na mesma or-
dem em que foram inseridos. Este tipo de estrutura é dito serFIFO (primeiro a
entrar, primeiro a sair). Em outros, a ordem de recuperação é inversa à ordem
de inserção (LIFO, ouultimo a entrar primeiro a sair). E algumas listas permi-
tem que você recupere os elementos em uma ordem completamente diferente.
Implementação — Os conceitos de tipo de acesso e ordenação estão fortemente
relacionados à implementação que você escolher para sua lista. Se você optar
por representar a lista vinculando cada elemento ao próximo, obterá um resul-
tado completamente diferente doponto de vista de acesso em vez de uma im-
plementação baseada em uma matriz indexada. Ou se você optar por vincular
cada elemento ao próximo e ao anterior, obterá uma lista que pode ser aces-
sada de ambas as extremidades.
A Figura 5.1 mostra diferentes tipos de listas que oferecem diferentes tipos de
acesso. Esta figura mostra o princípio por trás de cada tipo de lista, mas não a ma-
neira como elas sãoimplementado.
Figura 5.1 Diferentes tipos de listas oferecem diferentes tipos de acesso aos seus elementos.
O(1)—O tempo necessário para uma operação é constante. (Você pode pensar
nisso como significando que o tempo para um elemento é multiplicado por 1
para n elementos.)
O(log( n ))—O tempo para uma operação em n elementos é o tempo para um
elemento multiplicado por log( n ).
O( n )—O tempo para n elementos é o tempo para um elemento multiplicado
por n .
O( n 2 )—O tempo para n elementos é o tempo para um elemento multiplicado
por n 2 .
Seria ideal criar uma estrutura de dados com desempenho O(1) para todos os ti-
pos de operações. Infelizmente, isso ainda não é possível. Cada tipo de lista ofe-
rece um desempenho diferente para diferentes operações. Listas indexadas forne-
cem desempenho O(1) para recuperação de dados e próximo a O(1) para inserção.
As listas vinculadas individualmente oferecem desempenho O(1) para inserção e
recuperação em uma extremidade e O(n) na outra extremidade.
Você viu que escolher uma implementação para uma estrutura de dados geral-
mente é uma questão de negociar o tempo contra o tempo. Você escolherá uma
implementação mais rápida em algumas operações, mas mais lenta em outras,
com base nas operações mais frequentes. Mas na hora de negociar, há outras deci-
sões a serem tomadas.
Imagine que você deseja uma estrutura da qual os elementos possam ser recupe-
rados em uma ordem de classificação, o menor primeiro. Você pode optar por
classificar os elementos na inserção ou pode preferir armazená-los à medida que
chegam e procurar o menor apenas na recuperação. Ao decidir qual usar, um cri-
tério importante seria se o elemento recuperado é sistematicamente removido da
estrutura. Caso contrário, pode ser acessado várias vezes sem remoção. Nesse
caso, provavelmente seria melhor ordenar os elementos no momento da inserção
para evitar ordená-los várias vezes na recuperação. Este caso de uso corresponde
ao que é chamado defila de prioridade na qual você está esperando por um deter-
minado elemento. Você pode testar a fila várias vezes até que o elemento espe-
rado seja retornado. Esse caso de uso requer que os elementos sejam classificados
no momento da inserção.
Mas e se você quiser acessar elementos por várias ordens de classificação diferen-
tes? Por exemplo, você pode querer acessar os elementos na mesma ordem em
que foram inseridos ou na ordem inversa. O resultado pode corresponder à lista
duplamente encadeada da figura 5.1 . Parece que, nesse caso, os elementos devem
ser classificados no momento da recuperação.
Você pode favorecer uma ordem, levando a um tempo de acesso O(1) de uma
ponta e O( n ) da outra ponta, ou pode inventar uma estrutura diferente, talvez
dando tempo de acesso O(log( n )) de ambas as pontas. Outra solução seria arma-
zenar duas listas, uma na ordem de inserção e outra na ordem inversa. Dessa
forma, você teria um tempo de inserção mais lento, mas recuperação O(1) de am-
bas as extremidades. Uma desvantagem é que essa abordagem provavelmente
usaria mais memória, portanto, você pode ver que escolher a estrutura certa tam-
bém pode ser uma questão de trocar o tempo pelo espaço de memória.
Mas você também pode inventar alguma estrutura minimizando o tempo de in-
serção e o tempo de recuperação de ambas as extremidades. Esses tipos de estru-
turas já foram inventados e você só precisaria implementá-las, mas essas estrutu-
ras são muito mais complexas que as mais simples, então você estaria trocando
tempo por complexidade.
5.3.2 Evitando mutação no local
A maioria das estruturas de dados muda com o tempo porque os elementos são
inseridos e removidos. Para lidar com essas operações, você pode usar duas abor-
dagens. O primeiro é atualizar no local .
Atualizar no local
Se você precisa de listas imutáveis mais eficientes do que as oferecidas pelo Ko-
tlin, é discutível. Depende principalmente do seu caso de uso. Se você precisa de
uma estrutura LIFO imutável e de alto desempenho, como uma pilha, sem dúvida
precisa de algo mais eficiente do que a lista imutável Kotlin. Mas, de qualquer
forma, mesmo que você não precise de uma lista LIFO de alto desempenho,
aprender a criá-la é fundamental se você deseja escrever programas mais segu-
ros. Cada função que você criará para trabalhar em listas persistentes imutáveis
enriquecerá seu conhecimento básico do assunto a tal ponto que você não poderá
evitá-lo.
ComoEu disse, fazer uma cópia da estrutura de dados antes de inserir um ele-
mento é uma operação demorada que leva a um desempenho ruim. Mas este não
é o caso se você usarcompartilhamento de dados , que é a técnica na qual as estru-
turas de dados persistentes imutáveis são baseadas. A Figura 5.2 mostra como os
elementos podem ser removidos e adicionados para criar uma nova lista encade-
ada imutável com desempenho ideal.
Como você pode ver na figura, nenhuma cópia ocorre. Essa lista pode ser muito
mais eficiente para remover e inserir elementos do que uma lista mutável. Não,
as estruturas de dados funcionais (imutáveis e persistentes) nem sempre são mais
lentas que as mutáveis. Eles geralmente são ainda mais rápidos (embora possam
ser mais lentos em algumas operações). De qualquer forma, eles são muitomais
seguro.
Você já encontrou um elemento genérico composto por dois elementos de tipos di-
ferentes: o Pair . Uma lista vinculada individualmente de elementos de tipo A é,
na verdade, um arquivo Pair<A, List<A>> . o Pair class não está aberta para
extensão, mas você pode definir sua própria classe:
Em vez de usar a Pair com propriedades first e second , você criará uma
List classe específica com duas propriedades: head e tail . Isso simplifica o
manuseio do Nil caso. Nil poderia ser declarado como um object , o que signi-
fica que seria um singleton porque é necessárioser apenas uma instância da lista
vazia. Nesse caso, você o criaria como um List<Nothing> que pode ser conver-
tido em uma lista de qualquer tipo. Você pode definir esses elementos como
objeto complementar {
operador
fun <A> invocar(vararg az: A): List<A> = ⑦
az.foldRight(Nil as List<A>) { a: A, list: List<A> ->
Cons(a, lista) ⑧
}
}
}
Na listagem, a List classe é implementada como uma classe selada . Classes sela-
das permitem definirtipos de dados algébricos (ADT), ou seja, tipos que possuem
um conjunto limitado de subtipos. As classes seladas são implicitamente abstratas
e seu construtor é implicitamente privado. Aqui a List classe é parametrizada
com o parâmetro type A , que representa o tipo dos elementos da lista.
A List classe contém duas subclasses privadas para representar as duas formas
possíveis que uma List pode assumir: Nil para uma lista vazia e Cons para
uma lista não vazia. (O nome Cons significa construct .) A Cons classe recebe
como parâmetros an A (a cabeça) e a List<A> (arabo). Por convenção, o toS-
tring A função sempre inclui NIL como último elemento, então a implementa-
ção retorna [NIL] . Esses parâmetros são declarados internal para que não se-
jam visíveis de fora do arquivo ou do módulo no qual a List classe é declarada.
As subclasses foram feitas private , então você deve construir listas através de
chamadas para a função do objeto companheiro invoke .
A List classe também define o resumo isEmpty() função, que retorna true se
a lista estiver vazia e false caso contrário. o invoke function, juntamente com o
modificador operator , permite chamar a função com uma sintaxe simplificada:
Esta não é uma chamada para o construtor (o List construtor é privado), mas
para a função de objeto complementar invoke . o foldRight A função nesta lis-
tagem é uma função Kotlin padrão para matrizes e coleções. Você já definiu essa
função no capítulo 4, mas aprenderá mais sobre ela mais adiante emistocapítulo.
5.5 Compartilhamento de dados em operações de lista
Exercício 5.1
Dica
Esta função pode ter a mesma implementação para ambas as subclasses, então
você pode defini-la como uma função concreta na List classe.
Solução
Esta função cria uma nova lista que usa a lista atual como cauda e o novo ele-
mento como cabeça:
Dica
Você não pode alterar o cabeçalho de uma lista vazia; portanto, nesse caso, você
deve lançar uma exceção. (No próximo capítulo, você aprenderá como lidar com
esse caso com segurança.)
Solução
Como você pode ver, não é necessário converter explicitamente List em Nil e
Cons . Isso é feito automaticamente pelo Kotlin. Observe também que, graças ao
uso de uma classe selada, você não precisa de um else cláusula. Kotlin vê que to-
das as subclasses são processadas.
Se você é um ex-programador Java, talvez não goste desse estilo de programação.
Aqui, is é o equivalente a usar o Java instanceof operador, o que geralmente é
considerado uma prática ruim. No entanto, não há nada inerentemente errado
em usar instanceof em Java. É considerado uma prática ruim porque não é ori-
entado a objetos.
Por outro lado, verificar os tipos só porque não é proibido também não é uma boa
opção. Usar a ferramenta certa para o trabalho inclui o uso de técnicas OOP
quando essas podem ser melhores. É este o caso aqui? À primeira vista, pode pa-
recer que não. Vamos descobrir o porquê criando um resumo setHead função na
List classe pai com implementações separadas em Nil e Cons :
...
}
classe privada Cons<A>(cabeçalho do val interno: A,
cauda val interna: List<A>): List<A>() {
Isso parece bom, mas não é. Se você tentar chamar setHead uma lista vazia, re-
ceberá um ClassCastException em vez do esperado IllegalStateExcep-
tion . Isso ocorre porque quando um A é recebido pela setHead função da
Nil classe, ele é convertido em Nothing porque Nothing é o tipo de parâmetro
da classe setHead implementação da função. Isso faz com que um ClassCas-
tException as Nothing não seja um supertipo de A . (Pelo contrário, A é um su-
pertipo de Nothing .) Você pode usar o Nothing tipo para compilar o código,
mas não pode ser instanciado em tempo de execução. Como resultado, nenhuma
função tomada Nothing como um tipo de parâmetro pode ser chamada.
Isso significa que não há como implementar setHead como uma função abstrata
na List classe? Não. Uma solução simples consiste em declarar uma
Empty<A> classe abstrata e um Nil<Nothing> objeto implementando essa
classe. Esta seria uma escolha melhor se você tivesse muitas funções para definir,
cada uma recebendo um A argumento. Mas por enquanto, setHead e cons são
os únicos. Além disso, a cons função pode ter a mesma implementação para am-
bas as subclasses, então você pode mantê-la assim. No capítulo 11, você usará
uma classe abstrata com uma implementação de objeto singleton para
representarárvores.
5.6 Mais operações de lista
Você pode contar com o compartilhamento de dados para implementar várias ou-
tras operações de maneira eficiente - geralmente muito mais eficiente do que
pode ser feito com listas mutáveis. No restante desta seção, você adicionará funci-
onalidade à lista vinculada com base no compartilhamento de dados.
Exercício 5.3
Retornar a tail propriedade de uma lista tem o mesmo efeito que remover o pri-
meiro elemento, embora não ocorra nenhuma mutação. Escreva uma função mais
geral, drop , que remove os primeiros n elementos de uma lista. Esta função não
remove os elementos, mas retorna uma nova lista correspondente ao resultado
pretendido. Esta lista não será nada de novo porque você usará o compartilha-
mento de dados, então nada será criado. A Figura 5.4 mostra como você deve
proceder.
Figura 5.4 Eliminando os n primeiros elementos de uma lista sem alterar ou criar nada
Caso n seja maior que o comprimento da lista, basta retornar uma lista vazia.
(Talvez você prefira chamar essa função dropAtMost .)
Dica
Você deve usar corecursion para implementar a drop função. Você pode imple-
mentar uma função abstrata na List classe com duas implementações diferentes
em Nil e Cons . ou há uma solução melhor?
Solução
Mas isso aumenta a pilha para n mais de alguns milhares (algo entre 10.000 e
20.000), desde que a lista seja longa o suficiente. Como você viu no capítulo 4, você
deve mudar a recursão para corecursão adicionando um parâmetro adicional.
Isso parece fácil, e a seguinte solução pode vir à mente:
No entanto, isso não funcionará se n for maior que o comprimento desta lista. O
uso de corecursion evita que o caso terminal seja entregue ao Nil implementa-
ção. Nesse caso, você precisa testar explicitamente Nil e não pode confiar no po-
limorfismo. Mas não depender do polimorfismo significa que você não precisa de-
clarar uma função abstrata na List classe com duas implementações diferentes.
A seguinte função na List classe fará o trabalho:
fun drop(n: Int): List<A> {
tailrec fun drop(n: Int, list: List<A>): List<A> =
if (n <= 0) lista senão quando (lista) {
é Cons -> drop(n - 1, list.tail)
é Nil -> lista
}
return drop(n, this)
}
colocandofunções em classes é uma escolha. Você também pode definir sua fun-
ção fora da classe no nível do pacote. Como mencionei, declarar uma função den-
tro de uma classe é exatamente o mesmo que adicionar this seus parâmetros.
Você poderia, portanto, definir a drop função como
lista de classes<A> {
...
}
lista de classes<A> {
...
}
Uma desvantagem é que agora você deve declarar a classe Nil and em vez de
porque, ao contrário de Java, a classe envolvente não tem acesso a classes priva-
das internas ou aninhadas. Não é grande coisa, no entanto. Isso pode ser resolvido
declarando a função no Cons internal private objeto companheiro de classe :
objeto complementar {
...
}
Com esta solução, você pode chamar a função prefixando seu nome com o nome
da classe, como fazem os programadores Java ao chamar funções estáticas:
fun main(args: Array<String>) {
val lista = Lista(1, 2, 3)
println(Lista.drop(lista, 2))
}
Alternativamente, você pode importar a função. Por outro lado, as funções de ins-
tância geralmente são mais fáceis de usar do que as funções definidas no nível do
pacote ou em objetos complementares. Isso ocorre porque as funções de instância
permitem compor chamadas de função usando notação de objeto, que é muito
mais fácil de ler. Por exemplo, se você deseja descartar dois elementos de uma
lista de números inteiros e substituir o primeiro elemento do resultado por 0,
pode usar funções de nível de pacote:
Cada vez que você adiciona uma função ao processo, o nome da função é adicio-
nado à esquerda e os argumentos adicionais, além da própria lista, são adiciona-
dos à direita, conforme mostra a figura 5.5 .
Figura 5.5 Sem a notação de objeto (esquerda), as funções compostas podem ser difíceis de ler. O uso da nota-
ção de objeto (à direita) resulta em um código muito mais legível.
Agora você tem o melhor dos dois mundos. Você não precisa de uma função auxi-
liar (ou pode considerar que a função no objeto complementar é a função auxi-
liar) e não precisa de uma implementação específica em cada subclasse. Se você
não deseja que a função no objeto complementar seja acessível de fora, pode
torná-la privada. Nesse caso, você também pode colocá-lo na classe de lista e vol-
tar ao ponto de partida.
Quanto à questão de qual solução escolher, não há uma resposta única. Cabe a
você escolher a solução que se adapta às suas necessidades ou ao seu estilo. Uma
solução com menos código geralmente é preferível, porque quanto mais código
você tiver, mais terá que manter. Além disso, tente minimizar as partes visíveis.
Exercício 5.4
Solução
Supondo que você escolheu a abordagem de objeto complementar, veja como im-
plementar a função auxiliar:
privado
tailrec fun <A> dropWhile(lista: Lista<A>,
p: (A) -> Boolean): List<A> = when (list) {
Nil -> lista
is Cons -> if (p(list.head)) dropWhile(list.tail, p) else list
}
UMAoperação comum em listas consiste em adicionar uma lista a outra para for-
mar uma nova lista contendo todos os elementos de ambas as listas. Seria bom po-
der vincular as duas listas, mas isso não é possível. A solução é adicionar todos os
elementos de uma lista à outra lista. Masos elementos só podem ser adicionados à
frente (head) da lista, portanto, se você deseja concatenar list1 a list2 , deve
começar adicionando o último elemento de list1 à frente de list2 , conforme
indicado na figura 5.6 .
Figura 5.6 Compartilhando dados por concatenação. Você pode ver que ambas as listas são preservadas e que
list2 é compartilhada pela lista resultante. Mas você também pode ver que não pode proceder exatamente
como está indicado na figura porque teria que acessar primeiro o último elemento de list1. Isso não é possí-
vel devido à estrutura da lista.
A beleza dessa solução (para alguns leitores) é que você não precisa de uma figura
para expor como ela funciona porque não está funcionando. É uma definição ma-
temática traduzida em código.
A principal desvantagem dessa definição (para outros leitores) é que, pelo mesmo
motivo, ela não pode ser facilmente representada em uma figura. Isso pode soar
como humor, mas não é. esteA solução não representa o processo de concatena-
ção de listas (a partir das quais você pode desenhar um fluxograma). Ele expressa
o resultado diretamente na forma de uma expressão. E este código não calcula o
resultado. É o resultado!
NOTA Programar com funções como um substituto para estruturas de controle ge-
ralmente envolve pensar em termos de qual é o resultado pretendido, em vez de
como obtê-lo. Código funcional é uma tradução direta de uma definição em
código.
Obviamente, esse código transbordará a pilha se list1 for muito longo, embora
você nunca tenha um problema de pilha com o comprimento de list2 . A con-
sequência é que você não terá que se preocupar se tiver o cuidado de adicionar
apenas listas pequenas no início de listas de qualquer tamanho.
Um ponto importante a ser observado é que o que você está fazendo é adicionar
elementos da primeira lista em ordem inversa à frente da segunda lista. Isso é ob-
viamente diferente do entendimento de senso comum de concatenação: adicionar
a segunda lista ao final da primeira. Definitivamente, não é assim que funciona
com a lista encadeada individualmente.
Você deve ter notado que a complexidade dessa operação (e, portanto, o tempo
que ela levará para ser executada pelo Kotlin) é proporcional ao comprimento da
primeira lista. Se você concatenar list1 e list2 de comprimento n1 e n2 , a
complexidade é O(n1) , o que significa que é independente de n2 . Dependendo
de n1 e n2 , esta operação pode ser muito mais eficiente do que concatenar duas
listas mutáveis no imperativoprogramação.
Isso éàs vezes é necessário remover elementos do final de uma lista. Embora a
lista encadeada individualmente não seja a estrutura de dados ideal para esse
tipo de operação, você ainda deve ser capaz de implementá-la.
Exercício 5.5
Escreva uma função para remover o último elemento de uma lista. Esta função
deve retornar a lista resultante. Implemente-o como uma função de instância com
a seguinte assinatura:
Você pode estar se perguntando por que isso é chamado init em vez de algo co-
mo dropLast . O termo vem de Haskell (
http://zvon.org/other/haskell/Outputprelude/init_f.html ).
Dica
Pode haver uma maneira de expressar essa função em termos de outra, da qual já
falei. Talvez agora seja o momento certo para criar a função auxiliar.
Solução
Para remover o último elemento, você precisa percorrer a lista (de frente para
trás) e construir a nova lista (de trás para frente) porque o último elemento de
uma lista deve ser Nil . Isso é uma consequência da maneira como as listas são
criadas com Cons objetos. Isso resulta em uma lista com os elementos em ordem
inversa, portanto, a lista resultante deve ser invertida. Isso significa que você só
precisa implementar uma reverse função:
Observe que você não pode usar a List() sintaxe para chamar o invoke função
sem um argumento. Você deve chamá-lo explicitamente; caso contrário, Kotlin
pensa que você está chamando um construtor e lança uma exceção porque a
List classe é abstrata. Lembre-se, você pode escolher uma organização diferente,
como colocar a função auxiliar dentro da função de chamada:
Estas são as implementações para a Cons classe. Na Nil classe, a init função
lança umexceção.
5.6.4 Usando recursão para dobrar listas com funções de ordem superior
(HOFs)
Dentrono capítulo 4, você aprendeu como dobrar listas; folding também se aplica
a listas persistentes. Mas com listas mutáveis, você pode optar por implementar
essas operações por meio de iteração ou recursivamente. Com listas persistentes,
não há razão para usar a abordagem iterativa. Vamos considerar as operações co-
muns de dobra em listas de números.
Exercício 5.6
Escreva uma função para calcular a soma de todos os elementos de uma lista per-
sistente de números inteiros usando recursão. A implementação pode ser colo-
cada no objeto complementar List ou no nível do pacote no mesmo arquivo se
você criar as subclasses internal em vez de private . Colocá-lo no nível do pa-
cote pode parecer mais apropriado porque é específico para List<Int> .
Solução
Mas isso não vai compilar porque Nil não é um subtipo de List<Int> .
oO problema que você está tendo é que, embora o Nothing tipo seja um filho de
todos os tipos ( Int incluído), você sempre pode converter um Nothing em qual-
quer outro tipo, mas não pode converter um List<Nothing> em um
List<Int> . Se você se lembra do que aprendeu no capítulo 2, isso se deve ao
fato de que List é invariante em A . Para fazê-lo funcionar como esperado, você
deve fazer List covariant in A , o que significa que você deve declará-lo como
List<out A> :
Se você fizer isso, a List classe não compila mais, exibindo a seguinte mensagem
de erro (muitos erros semelhantes ocorrem em outras linhas):
Isso significa que a List classe não pode conter funções com um parâmetro do
tipo A . O parâmetro é uma entrada para a função, então está na in posição. As
funções só podem ter um A como tipo de retorno (a out posição).
Entendendo a variância
Para entender a variação, você pode pensar em uma cesta de maçãs. Você pode
realizar dois tipos principais de operações em relação a esta cesta:
Coloque uma maçã na cesta
Tire uma maçã da cesta
Você pode colocar um Gala em a Basket of Apple , mas não pode fazer isso
com a Fruit porque pode não ser um Apple . Por outro lado, se você precisar de
um Fruit , poderá retirá-lo de um Basket de Apple . Mas se você quiser um
Gala , não poderá fazer isso porque o Basket pode conter outras variedades de
Apple .
Mas e um vazio Basket ? Não seria útil poder dizer que está vazio de tudo o que
você precisa? Se você não fosse capaz de fazer isso, precisaria de uma cesta vazia
para as maçãs, outra para as laranjas e ainda outra para cada tipo de objeto
possível.
Você poderia usar uma cesta vazia de Any (o equivalente Kotlin de Java Object ).
Mas, embora você pudesse colocar qualquer coisa na cesta vazia, não poderia
(utilmente) tirar nada dela porque nunca saberia que tipo de objeto obteria. A so-
lução Kotlin para esse problema é o Nothing type. Ao contrário Any de , que é o
tipo pai de todos os outros tipos, Nothing é um tipo filho de todos os tipos.
Você pode pensar que isso não é justo porque sabe com certeza que adicionar um
A a um List<A> está correto. Mas aqui você está errado. Não está nada bem.
Para demonstrar o porquê, pense no que seria a implementação de uma
cons função abstrataem cada subclasse:
Se você tentar isso, receberá um erro adicional (além do anotado) na Nil imple-
mentação: a implementação da cons função é marcada pelo compilador comocó-
digo inacessível . Por que é isso? Porque this na Nil classe refere-se a a
List<Nothing> e chamar Nil.cons(1) faria com 1 que fosse convertido em
Nothing . Isso não é possível porque Nothing é um subtipo de Int e não o con-
trário. Agora você tem dois problemas para resolver:
O compilador não permite que você use A na in posição, embora você saiba
que seria válido em alguns casos
O problema de elenco na Nil aula, que você deve evitar para eliminar um
caso em que surgiria o primeiro problema
Para entender o que está acontecendo no Nil class, você deve se lembrar que Ko-
tlin é uma linguagem estrita , o que significa que os argumentos da função são
avaliados independentemente de serem usados ou não. O problema não está na
implementação da função:
Aqui, você está dizendo ao compilador que ele não deve se preocupar com um
problema de variância na cons função: você está assumindo a responsabilidade
e se algo der errado, a culpa é sua. Agora, você pode aplicar a mesma técnica para
funções setHead e concat :
...
Você só precisa fazer isso para funções que recebem um parâmetro do tipo A , ou
List<A> . Você não precisa fazer isso para funções sem parâmetros, nem para
o dropWhile função recebendo um parâmetro do tipo (A) -> Boolean . Mas
você também deve garantir que, ao usar esse truque, nenhum lançamento inse-
guro falhará. Como sempre, com maior liberdade vem maior responsabilidade.
Note que existe outra possibilidade que consiste em criar uma Empty<A> classe
abstrata para representar listas vazias e então criar um Nil<Nothing> objeto
singleton. Isso permite definir funções abstratas na List classe pai com imple-
mentação específica em Cons ou Empty . Veja como a concat função pode ser
definida:
De qualquer forma, graças ao uso da out variância, sua sum função agora está
perfeitamente correta!
Exercício 5.7
Escreva uma função para calcular o produto de todos os elementos de uma lista
de doubles usando recursão.
Solução
Mas o que deve retornar para uma lista vazia? Se você se lembra de seus cursos
de matemática, saberá a resposta. Caso contrário, você pode encontrar a resposta
no requisito anterior para uma lista não vazia.
Considere o que acontecerá quando você aplicar a fórmula recursiva a todos os
elementos. Você terá um resultado que terá que ser multiplicado pelo produto de
todos os elementos de uma lista vazia. Como você deseja obter esse resultado, não
tem escolha a não ser dizer que o produto de todos os elementos de uma lista va-
zia é 1.
Esta é a mesma situação do sum exemplo, quando você usa 0 como a soma de to-
dos os elementos de uma lista vazia. 0 é a identidade, ou elemento neutro, para a
operação de soma e 1 é a identidade, ou elemento neutro, para o produto. Su-
a product função pode ser escrita da seguinte forma:
Mas esqueça esta versão otimizada e veja as definições para sum e product .
Você consegue detectar um padrão que pode ser abstraído? Vejamos essas funções
lado a lado (depois de alterar o nome do parâmetro):
As duas funções são as mesmas com alguns valores diferentes para Type , ope-
ration , identity e operator . Se você puder encontrar uma maneira de abs-
trair essas partes comuns, terá que fornecer as informações variáveis para imple-
mentar ambas as funções sem se repetir. Essa função comum é chamada defold ,
que você estudou no capítulo 4. Nesse capítulo, você aprendeu que existem dois ti-
pos de dobras: foldRight e foldLeft , bem como uma relação entre essas duas
operações.
A Listagem 5.2 mostra as partes comuns das operações de soma e produto abstraí-
das em uma função chamada foldRight , tendo como parâmetros a lista a do-
brar, um elemento identidade e um HOF (Função de Ordem Superior) represen-
tando a operação usada para dobrar a lista. O elemento identidade é obviamente
a identidade para a operação dada, e a função está na forma curried. (Consulte o
capítulo 3 se não lembrar o que significa curried .) A listagem a seguir mostra a
função que representa a parte do operador do seu código.
① A e B representam os tipos.
a Type variávelpart foi substituído por dois tipos aqui, A e B . Isso ocorre porque
o resultado da dobra nem sempre é do mesmo tipo dos elementos da lista. Aqui
está abstraído um pouco mais do que o necessário para as operações de soma e
produto, mas isso será útil em breve. o operation parte variável são os nomes
das duas funções.
A operação de dobra não é específica para cálculos aritméticos. Você pode usar
uma dobra para transformar uma lista de caracteres em uma string. Nesse caso,
A e B são dois tipos diferentes: Char e String . Mas você também pode usar
uma dobra para transformar uma lista de strings em uma única string. Você pode
ver agora como você poderia implementar concat ?
A propósito, foldRight é semelhante à própria lista encadeada individualmente.
Se você pensar na lista 1, 2, 3 como
Mas talvez você já tenha percebido que Nil é a concatenação da lista de identida-
des, embora você possa passar sem ela, desde que a lista de listas a serem concate-
nadas não esteja vazia. Nesse caso, é chamado de reduzir em vez de dobrar . Isso
só é possível porque o resultado é do mesmo tipo que os elementos. Pode ser colo-
cado em prática passando Nil e como cons a foldRight identidade e a função
que se usa para dobrar:
Isso produz uma nova lista com os mesmos elementos na mesma ordem, como
você pode ver executando o seguinte código:
[1, 2, 3, NIL]
Exercício 5.8
Escreva uma função para calcular o comprimento de uma lista. Esta função usará
a foldRight função.
Solução
Essa implementação, além de ser recursiva (o que significa que pode estourar a
pilha para listas longas), tem desempenho ruim. Mesmo se transformado em core-
cursivo, ainda é 0( n ), o que significa que o tempo necessário para retornar o
comprimento é proporcional ao comprimento da lista. Nos próximos capítulos,
você verá como obter o comprimento de uma lista encadeada em tempo
constante.
Exercício 5.9
A foldRight função usa recursão, mas não é recursiva de cauda, portanto, rapi-
damente transbordará a pilha. A rapidez depende de vários fatores, sendo o mais
importante o tamanho da pilha. Em vez de usar foldRight , crie um fol-
dLeft função que é corecursiva e segura em pilha. Aqui está sua assinatura:
public <B> foldLeft(identity: B, f: (B) -> (A) -> B): B
Dica
Solução
tailrec fun <A, B> foldLeft(acc: B, list: List<A>, f: (B) -> (A) -> B): B =
quando (lista) {
List.Nil -> acc
é List.Cons -> foldLeft(f(acc)(list.head), list.tail, f)
}
Use sua nova foldLeft função para criar novas versões de pilha segura de sum ,
product e length .
Solução
Mais uma vez, o segundo parâmetro da função length (que representa cada ele-
mento da lista em cada chamada da função) é ignorado. O primeiro parâmetro é
representado pelo it palavra-chave. Como resultado, você não pode remover o
parâmetro sublinhado sem usar um nome explícito para o primeiro parâmetro,
como
fun length(): Int = foldLeft(0) { i -> { i + 1} }
Essa função é quase tão ineficiente quanto a foldRight versão e não deve ser
usada no código de produção. Embora não exagere na pilha, é lento porque deve
contar os elementos da lista cada vez que é chamado.
Exercício 5.11
Use foldLeft para escrever uma função para inverter uma lista.
Dica
Solução
Inverter uma lista por meio de uma dobra à esquerda é simples, partindo de uma
lista vazia como acumulador e adicionando sucessivamente cada elemento da pri-
meira lista ao acumulador por meio de uma chamada à cons função:
Como você pode ver, você precisa especificar o tipo de parâmetro Nil e convertê-
lo em um arquivo List . Caso contrário, Kotlin usa Nil como tipo de retorno. Na
versão anterior de reverse , você usava uma função auxiliar usando a
List como argumento, de modo que a conversão ocorria implicitamente. Você
não pode usar List() diretamente porque, na List classe, este seria o constru-
tor da classe, o que é impossível porque a classe é abstrata. Outra solução para
contornar esse problema é chamar o invoke função explicitamente:
Exercício 5.12
Dica
Solução
Essa implementação pode ser útil para obter uma versão segura para pilha, mas
mais lenta, de foldRight :
Exercício 5.13
Use o que você aprendeu no capítulo 4 para escrever uma versão correcursiva da
foldRight função sem usar foldLeft explicitamente. Ligaresta função coFol-
dRight .
Dica
Solução
Exercício 5.14
A concat função pode ser implementada facilmente usando uma dobra à direita:
Outra solução é usar uma dobra à esquerda. Neste caso, a implementação será a
mesma da reverse via foldLeft aplicada à primeira lista invertida, utilizando
a segunda lista como acumulador. Na implementação a seguir, observe o uso de
uma referência de função ( x::cons ):
Se você achar que usar uma referência de função torna o código menos legível,
você pode usar um lambda:
Exercício 5.15
Escreva uma função para nivelar uma lista de listas em uma lista contendo todos
os elementos de cada lista contida.
Dica
Solução
Mais uma vez, você pode usar uma referência de função em vez de um lambda
para representar a segunda parte da função: { x -> x::concat } é equiva-
lente a { x -> { y -> x.concat(y) } } :
Para tornar esta função segura em pilha, você pode usar o coFoldRight função
em vez de foldRight :
fun <A> flatten(lista: Lista<Lista<A>>): Lista<A> =
list.coFoldRight(Nil) { x -> x::concat }
Vocêpode definir muitas abstrações úteis para trabalhar em listas. Uma abstração
consiste em alterar todos os elementos de uma lista aplicando uma função co-
mum a eles.
Exercício 5.16
Escreva uma função que receba uma lista de números inteiros e multiplique cada
um deles por 3.
Dica
Tente usar as funções que você definiu até agora. Não use (co)recursão explicita-
mente. O objetivo é abstrair a recursão de uma vez por todas, para que você possa
colocá-la em funcionamento sem precisar reimplementá-la todas as vezes.
Solução
Você precisa aplicar foldRight com uma lista vazia como valor de identidade e
uma função que adiciona cada elemento multiplicado por 3 a uma lista:
Você precisa definir o tipo explicitamente, seja para a identidade ou para o t pa-
râmetro. Isso se deve à capacidade limitada do Kotlin em relação à inferência de
tipos. Você também pode usar uma dobra à esquerda, o que tornaria a pilha de
funções segura, mas também inverteria a lista, então você precisaria reverter o
resultado.
Exercício 5.17
Solução
Esta operação pode ser vista como uma concatenação de uma lista vazia do tipo
esperado ( List<String> ) com a lista original, com cada elemento sendo trans-
formado antes de adicioná-lo ao acumulador. Como resultado, a implementação é
semelhante ao que você fez com o concat função:
Escreva uma função geral de segurança de pilha map na List classe que permite
modificar cada elemento de uma lista aplicando uma função especificada a ele.
Desta vez, torne-a uma função de instância de List . Aqui está sua assinatura:
Solução
Para torná-lo seguro, você pode usar uma dobra à esquerda e inverter o
resultado:
Exercício 5.19
Escrevauma filter função que remove de uma lista os elementos que não satis-
fazem um determinado predicado. Mais uma vez, implemente isso como uma fun-
ção de instância com a seguinte assinatura:
Solução
Exercício 5.20
Escreva uma flatMap função que se aplique a cada elemento de List<A> , uma
função de A a List<B> , e retorne a List<B> . Sua assinatura será esta:
deve retornar
Lista(1,-1,2,-2,3,-3).
Solução
o flatMap A função pode ser vista como uma composição de map , que usa uma
função que retorna uma lista, e flatten , que converte uma lista de lista em uma
lista. A implementação mais simples (na List classe) é então
Exercício 5.21
Solução
Observe que há uma forte relação entre map , flatten, e flatMap . Se você ma-
pear uma função retornando uma lista para uma lista, obterá uma lista de listas.
Você pode então se inscrever flatten para obter uma única lista contendo todos
os elementos das listas anexas. Você obteria exatamente o mesmo resultado apli-
cando diretamente flatMap .
Resumo
1
Jim Gray, “O conceito de transação: virtudes e limitações”, Relatório Técnico 81.3
(Tandem Computers, junho de 1981),
http://www.hpl.hp.com/techreports/tandem/TR-81.3.pdf .