Professional Documents
Culture Documents
8 Advanced List Handling - The Joy of Kotlin
8 Advanced List Handling - The Joy of Kotlin
Nissocapítulo
No capítulo 5, você criou sua primeira estrutura de dados, a lista encadeada indi-
vidualmente. Nesse ponto, você não tinha todas as técnicas necessárias para tor-
nar essa estrutura uma ferramenta completa para manipulação de dados. Uma
ferramenta particularmente útil que estava faltando era alguma maneira de re-
presentar operações que produziam dados opcionais ou operações que produ-
ziam um erro.
Nesta implementação, a lista é dobrada por meio de uma operação que consiste
em adicionar 1 ao resultado para cada elemento da lista. O valor inicial é 0 e o va-
lor de cada elemento é ignorado. Isso é o que permite que você use a mesma defi-
nição para todas as listas. Como os elementos da lista são ignorados, o tipo do ele-
mento da lista é irrelevante. Você pode, no entanto, comparar a operação anterior
com uma que calcula a soma de uma lista de inteiros:
A principal diferença aqui é que a sum função só pode trabalhar com números in-
teiros, enquanto a length funçãofunciona para qualquer tipo. Observe que fol-
dRight e foldLeft são apenas maneiras de abstrair a recursão. O comprimento
de uma lista pode ser definido como 0 para uma lista vazia e 1 mais o compri-
mento da cauda para uma lista não vazia. Da mesma forma, a soma de uma lista
de inteiros pode ser definida recursivamente como 0 para uma lista vazia e o va-
lor da cabeça mais a soma da cauda para uma lista não vazia.
Existem outras operações que podem ser aplicadas às listas dessa forma e, entre
elas, várias para as quais o tipo dos elementos da lista é irrelevante:
O código hash de uma lista pode ser calculado adicionando os códigos hash de
seus elementos. Como o código hash é um número inteiro (pelo menos para ob-
jetos Kotlin), essa operação não depende do tipo do objeto.
A representação de string de uma lista, conforme retornada pela toString fun-
ção, pode ser calculado compondo a toString representação dos elementos da
lista. Mais uma vez, o tipo real dos elementos é irrelevante.
Todoessas funções podem ser implementadas usando uma dobra, mas essas im-
plementações têm uma grande desvantagem: o tempo necessário para calcular o
resultado é proporcional ao comprimento da lista. Imagine que você tem uma
lista de cerca de um milhão de elementos e deseja verificar o comprimento.
length Contar os elementos pode parecer o único caminho a percorrer (é isso
que a função baseada em dobrasfaz). Mas se você fosse adicionar elementos à
lista até chegar a um milhão, certamente não contaria os elementos após adicio-
nar cada um.
Em tal situação, você manteria uma contagem dos elementos em algum lugar e
adicionaria um a essa contagem toda vez que adicionasse um elemento à lista.
Talvez você tivesse que contar uma vez se estivesse começando com uma lista não
vazia, mas é isso. Você aprendeu essa técnica, memorização, no capítulo 4. A ques-
tão é: onde você pode armazenar o valor memorizado? A resposta é óbvia: na lis-
taem si.
Data structures implementing in-place mutation don’t have this problem. In a mu-
table list, nothing keeps us from memoizing the list length as a mutable integer,
which takes only 32 bits. But with an immutable list, you need to memoize the
length in each element. It’s difficult to know the exact increase in size, but if the
size of a singly linked list is around 40 bytes per node (for the nodes themselves)
plus two 32-bit references for the head and the tail (on a 32-bit JVM), this would
result in about 100 bytes per element. In this case, adding the length would cause
an increase of slightly over 30%. The result would be the same if the memoized
values were references, such as memoizing the maximum or minimum of a list of
Comparable objetos. Em uma JVM de 64 bits, é ainda mais difícil calcular devido
a alguma otimização no tamanho das referências, mas você entendeu.
NOTA Para obter mais informações sobre o tamanho das referências de objetos
na JVM, consulte a documentação da Oracle sobre ponteiros de objetos comuns
compactados e aprimoramentos de desempenho da JVM (
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance -melho-
rias-7.html ).
Cabe a você decidir se deseja usar memoização em suas estruturas de dados. Pode
ser uma opção válida para funções que são chamadas com frequência e não
criam novos objetos para seus resultados. Por exemplo, as funções
length e hashCode retornar números inteiros eas funções max ou min retor-
nam referências a objetos já existentes, portanto, esses podem ser bons candida-
tos. Por outro lado, a toString funçãocria novas strings que teriam que ser me-
morizadas, o que provavelmente seria um grande desperdício de espaço de me-
mória. O outro fator a ser levado em consideração é a frequência com que a fun-
ção é usada. a length funçãopode ser usado com mais frequência do que hash-
Code porque usar listas como chaves de mapa não é uma prática comum.
Exercício 8.1
Crie uma versão memorizada da length funçãodo exercício 3.8. Sua assinatura
na List aulaé
Solução
Para implementar a Cons versão, você deve primeiro adicionar o campo memoi-
zing à classe:
...
Esta versão será muito mais rápida que a original. Uma coisa interessante a notar
é a relação entre as funções length e isEmpty . Você pode ficar tentado a pensar
que isEmpty é equivalente a length == 0 , mas embora isso seja verdade do
ponto de vista lógico, pode haver uma grande diferença na implementação e no
desempenho.
A propósito, Kotlin permite uma solução muito mais compacta usando uma pro-
priedade abstrata. Kotlin gera automaticamente getters para val propriedades e
permite que você defina propriedades abstratas que são implementadas na classe
de extensão. A declaração na List classeé
Embora esta técnica de medição não seja precisa, a diminuição média da memó-
ria disponível é de cerca de 22 MB em ambos os casos (com ou sem memoização),
variando entre 20 MB e 25 MB. Isso mostra que o aumento teórico de 4 MB (um
milhão * 4 bytes) não é tão significativo quanto você esperaria. Por outro lado, o
aumento no desempenho é enorme. Perguntar a duração dez vezes pode custar
mais de 200 ms sem memorização. Com memorização, o tempo é 0 (tempo muito
curto para ser medido em milissegundos). Embora adicionar um elemento au-
mente o custo (adicionar um ao comprimento da cauda e armazenar o resultado),
remover um elemento não tem custo porque o comprimento da cauda éjámemo-
rizado.
No capítulo 7, você viu que Result e List são estruturas de dados semelhantes,
diferindo principalmente em sua cardinalidade, mas compartilhando algumas de
suas funções mais importantes, como map e flatMap . Você também viu como lis-
tas podem ser compostas com listas e resultados com resultados. Agora, você verá
como os resultados podem ser compostos com listas.
NoNeste ponto, você provavelmente deve ter notado que tento evitar acessar os
elementos de resultados e listas diretamente. Acessar o início ou o final de uma
lista lança uma exceção se a lista for Nil , e lançar uma exceção é uma das ma-
neiras mais diretas de tornar os programas inseguros. Mas você aprendeu que
pode acessar com segurança o valor em a Result fornecendo um valor padrão a
ser usado no caso de falha ou resultado vazio. Você pode fazer o mesmo ao aces-
sar o cabeçalho de uma lista? Não exatamente, mas você pode retornar um ar-
quivo Result .
Exercício 8.2
Dica
Solução
Exercício 8.3
Não use recursão explícita, mas tente desenvolver as funções que você desenvol-
veu no capítulo 5. Você deve ser capaz de definir uma única função na
List classe.
Solução
Você poderia resolver isso de várias maneiras. Mostrarei primeiro uma solução
trivial e depois discutirei seus problemas. Então mostrarei uma solução melhor
que evita esses problemas. Aqui está a solução trivial usando recursão explícita:
Esta solução tem vários problemas. Primeiro, é recursivo, então você deve trans-
formá-lo para torná-lo correcursivo. Isso é fácil, mas você terá que torná-lo uma
função usando a lista como argumento:
Uma solução melhor é usar uma dobra, que abstrai a recursão para você. Tudo o
que você precisa fazer é criar a função certa para dobrar. Você precisa sempre
manter o último valor, se existir. Esta pode ser a função a ser usada:
Exercício 8.4
Você pode substituir a headSafe função por uma única implementação usando
uma dobra na List classe? Quais seriam as vantagens e desvantagens de tal
implementação?
Solução
Usar foldRight aqui é exatamente o mesmo que inverter a lista e depois percor-
rer o resultado para encontrar o último elemento (que é o primeiro elemento da
lista original). Não muito eficiente! Aliás, é exatamente isso que a lastSafe fun-
çãofaz para encontrar o último elemento: inverte a lista e pega o primeiro ele-
mento do resultado. Exceto pela diversão, não há motivo para usar essa imple-
mentação. Mas se você preferir uma única implementação na List classe, você
pode usar o padrãoCoincidindo:
Jogue fora todas as falhas ou resultados vazios e produza uma lista da U lista
restante de sucessos. Se não houver sucesso na lista, o resultado pode conter
uma lista vazia.
Jogue fora todas as falhas ou resultados vazios e produza uma lista da U lista
restante de sucessos. Se não houver sucesso na lista, o resultado será um
fracasso.
Decida que todos os elementos devem ser bem-sucedidos para que toda a ope-
ração seja bem-sucedida. Construa uma lista de U com os valores se todos fo-
rem bem-sucedidos e retorne-a como a Success<List<U>> ou retorne a
Failure<List<U>> caso contrário.
Exercício 8.5
Dica
O nome escolhido para a função é uma indicação do que você precisa fazer.
Solução
O tipo desta função é (Result<A>) → List<A> . Tudo que você precisa fazer
agora é flatMap esta função para a lista de Result<A> como esta:
Exercício 8.6
Dica
Mais uma vez, use a foldRight função,recursão não explícita. Você também pre-
cisará da map2 função definida na Result classe.
Solução
import com.fpinkotlin.common.map2
...
Novamente, você pode preferir uma implementação segura em pilha com base
em foldLeft , desde que não se esqueça de inverter o resultado.
Exercício 8.7
Defina uma traverse função mais genérica que percorra uma lista A enquanto
aplica uma função de A a Result<B> e produz um arquivo Result<List<B>> .
Aqui está sua assinatura:
Dica
Não use recursão. Prefira a foldRight função, que abstrai a recursão para você
ou para o coFoldRight versão se você quiser torná-la segura para pilha.
Solução
Muitos casos de uso comuns para o List tipo de dados merecem ser abstraídos
para que você não precise repetir o mesmo código várias vezes. Você descobrirá
regularmente novos casos de uso que podem ser implementados combinando
funções básicas. Você nunca deve hesitar em incorporar esses casos de uso como
novas funções na List classe. Os exercícios a seguir mostram vários dos casos de
uso mais comuns:
Exercício 8.8
Escreva uma zipWith função que combine os elementos de duas listas de tipos
diferentes para produzir uma nova lista, dado um argumento de função. Aqui
está a assinatura:
Dica
Solução
Para este exercício, você deve usar a recursão explícita porque a recursão deve
ser feita em ambas as listas simultaneamente. Você não tem nenhuma abstração à
sua disposição para isso. Aqui está uma solução:
o zipWith função auxiliar corecursive é chamada com uma lista vazia como o
acumulador inicial. Se uma das duas listas de argumentos estiver vazia, a core-
cursion é interrompida e o acumulador atual é retornado. Caso contrário, um
novo valor é computado aplicando a função aos valores principais de ambas as
listas, e a função auxiliar é chamada recursivamente com as extremidades de am-
bas as listas de argumentos.
Exercício 8.9
Dica
Solução
Um dos casos de uso comuns para product e zipWith é usar um construtor para
a função de combinação. Aqui está um exemplo usando o Pair construtor:
[(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), NIL]
Exercício 8.10
Escreva uma unzip função para transformar uma lista de pares em um par de
listas. Aqui está sua assinatura:
Não use recursão explícita. Uma simples chamada para foldRight deve fazer o
trabalho.
Solução
Você precisa dobrar a lista à direita usando um par de duas listas vazias como a
identidade:
Exercício 8.11
Generalize a unzip função para que ela possa transformar uma lista de qualquer
tipo em um par de listas, dada uma função que recebe um objeto do tipo de ele-
mento lista como seu argumento e produz um par. Por exemplo, dada uma lista
de Payment instâncias, você deve ser capaz de produzir um par de listas: uma
contendo os cartões de crédito usados para efetuar os pagamentos e a outra con-
tendo os valores dos pagamentos. Implemente esta função como uma função de
instância List com a seguinte assinatura:
fun <A1, A2> unzip(f: (A) -> Pair<A1, A2>): Pair<List<A1>, List<A2>>
Dica
Solução
Uma coisa importante é que o resultado da função seja usado duas vezes. Para
não aplicar a função duas vezes, você pode usar um lambda multilinha:
fun <A1, A2> unzip(f: (A) -> Pair<A1, A2>): Pair<List<A1>, List<A2>> =
this.coFoldRight(Pair(Nil, Nil)) { a ->
{ listaPair: Par<Lista<A1>, Lista<A2>> ->
val par = f(a)
Pair(listPair.first.cons(pair.first),
listPair.second.cons(pair.second))
}
}
Uma solução mais inteligente é usar a let funçãoda biblioteca padrão Kotlin:
fun <A1, A2> unzip(f: (A) -> Pair<A1, A2>): Pair<List<A1>, List<A2>> =
this.coFoldRight(Pair(Nil, Nil)) { a ->
{ listaPair: Par<Lista<A1>, Lista<A2>> ->
f(a).let {
Pair(listPair.first.cons(it.first),
listPair.second.cons(it.second))
}
}
}
Você pode se perguntar por que essa solução é mais inteligente. Não há absoluta-
mente nenhuma razão, exceto que eu gosto mais. Caso contrário, fique à vontade
para usar a versão multilinha (que, aliás, deve ser imperceptivelmente mais rá-
pida). De qualquer forma, agora você pode redefinir a versão do exercício
8.10Como
No capítulo 5, você trabalhou com sua primeira estrutura de dados, a lista encade-
ada individualmente. A lista vinculada individualmente não é a melhor estrutura
para acesso indexado a seus elementos, mas às vezes é necessário usar o acesso
indexado. Como de costume, você deve abstrair tal processo em List funções.
Exercício 8.12
Desta vez, comece com uma versão explicitamente recursiva. Em seguida, tente
responder às seguintes perguntas:
Solução
Primeiro, você pode verificar o índice para ver se é positivo e menor que o com-
primento da lista. Se não for, retorne um Failure . Caso contrário, chame a fun-
ção auxiliar para processar a lista correcursivamente. Esta função verifica se o ín-
dice é 0. Se for, retorna o cabeçalho da lista recebida. Caso contrário, ele chama a
si mesmo recursivamente no final da lista com um índice decrementado.
O Nil caso na função auxiliar é um código morto. Se a lista for Nil , index sem-
pre será menor que 0 ou maior ou igual ao comprimento da lista (porque é 0).
Conseqüentemente, a cauda do argumento nunca será Nil . Você pode preferir a
seguinte versão:
Você também pode ficar tentado a usar a let funçãoda biblioteca padrão:
fun getAt(índice: Int): Resultado<A> {
tailrec fun <A> getAt(list: List<A>, index: Int): Result<A> = // Aviso
(listar como Contras).let {
se (índice == 0)
Resultado(lista.head)
senão
getAt(lista.cauda,
índice - 1)
}
Mas esta versão compila com um aviso porque a função auxiliar não é mais recur-
siva. Como consequência, pode estourar a pilha.
Usar corecursion parece ser a melhor solução possível. Mas como dobrar abstrai a
recursão, é possível usar uma dobra? Sim, é, e deveria ser uma dobra à esquerda,
mas a solução é complicada:
Primeiro você tem que definir o valor da identidade. Como esse valor deve conter
tanto o resultado quanto o índice, será um Pair segurando o Failure caso. Em
seguida, você pode verificar a validade do índice. Se for considerado inválido,
torne o resultado temporário igual a it (a identidade). Caso contrário, dobre para
a esquerda com uma função retornando o resultado já calculado ( ta ) se o índice
for menor que 0 ou um novo Success caso contrário. Essa solução pode parecer
mais inteligente, mas tem duas desvantagens:
Você pode achar que é menos legível. Isso é subjetivo, então cabe a você
decidir.
É menos eficiente porque continuará dobrando toda a lista mesmo depois de
encontrar o valor procurado.
Encontre uma solução que faça com que a versão baseada em dobra termine as-
sim que o resultado for encontrado.
Dica
Você precisará de uma versão especial do foldLeft para isso, bem como uma
versão especial do Pair .
Solução
Primeiro, você precisa de uma versão especial do foldLeft em que você pode es-
capar da dobra quando o elemento absorvente (ou elemento zero) da operação de
dobramento é encontrado. Pense em uma lista de números inteiros que você de-
seja dobrar multiplicando-os. O elemento de absorção para a multiplicação é 0, o
que significa que multiplicar qualquer número por 0 resulta em 0. Aqui está a de-
claração de uma versão de curto-circuito (ou fuga ) de foldLeft na List classe:
abstract fun <B> foldLeft(identity: B, zero: B, f: (B) -> (A) -> B): B
o elemento zero
substituir fun <B> foldLeft(identity: B, zero: B, f: (B) -> (A) -> B): B {
fun <B> foldLeft(acc: B,
zero:B,
lista: Lista<A>, f: (B) -> (A) -> B): B = quando (lista) {
Nil -> acc
é Contras ->
if (acc == zero)
acc
senão
foldLeft(f(acc)(list.head), zero, list.tail, f)
}
return foldLeft(identity, zero, this, f)
}
Como você pode ver, a única diferença é que se o valor do acumulador for 0 , a
recursão é interrompida e o acumulador é retornado. Agora você precisa de um
0 valor para sua dobra.
fun abstract <B> foldLeft(acc: B, p: (B) -> Boolean, f: (B) -> (A) -> B): B
A Nil implementação retorna identity como anteriormente:
}.primeiro
}
As vezesvocê precisará dividir uma lista em duas partes em uma posição especí-
fica. Embora a lista encadeada isoladamente esteja longe de ser ideal para esse
tipo de operação, ela é relativamente simples de implementar. A divisão de uma
lista tem várias aplicações úteis, entre as quais o processamento de suas partes
em paralelo usando vários threads.
Exercício 8.14
Escreva uma splitAt funçãoque recebe um Int como parâmetro e retorna duas
listas dividindo a lista na posição especificada. Não deveria haver nenhum Inde-
xOutOfBoundException s. Em vez disso, um índice abaixo 0 deve ser tratado
como 0 , e um índice acima de max deve ser tratado como o valor máximo do
índice.
Dica
Solução
Você consegue pensar em uma implementação usando uma dobra em vez de re-
cursão explícita?
Dica
Uma implementação que percorre toda a lista é fácil. Uma implementação percor-
rendo a lista apenas até que o índice seja encontrado é muito mais difícil. Ele pre-
cisará de uma nova versão especial foldLeft com escape, retornando o valor es-
capado e o restante da lista.
Solução
sobrepor
fun <B> foldLeft(identidade: B, zero: B, f: (B) -> (Nada) -> B):
Par<B, Lista<Nada>> = Par(identidade, Nil)
Por fim, você deve alterar a Cons implementação para retornar o restante da
lista:
substituir fun <B> foldLeft(identity: B, zero: B, f: (B) -> (A) -> B):
Par<B, Lista<A>> {
fun <B> foldLeft(acc: B, zero: B, list: List<A>, f: (B) -> (A) -> B):
Par<B, Lista<A>> =
quando (lista) {
Nil -> Pair(acc, lista)
é Cons -> if (acc == zero)
Par(acc, lista)
senão
foldLeft(f(acc)(list.head), zero, list.tail, f)
}
return foldLeft(identity, zero, this, f)
}
retornar quando {
índice <= 0 -> Pair(Nil, este)
index >= comprimento -> Pair(this, Nil)
senão -> {
identidade val = Pair(Nil as List<A>, -1)
val zero = Pair(este, índice)
val (par, lista) = this.foldLeft(identity, zero) { acc ->
{ e -> Par(acc.first.cons(e), acc.second + 1) }
}
Pair(pair.first.reverse(), lista)
}
}
}
O fato de ser possível usar uma dobra não significa que você deva fazê-lo. Os exer-
cícios anteriores são apenas isso: exercícios. Como designer de biblioteca, você
precisa escolher a implementação mais eficiente.
Uma boa biblioteca deve ter uma interface funcional e respeitar os requisitos
para uma programação segura. Isso significa que todas as funções devem ser ver-
dadeiras sem efeitos colaterais e todas devem respeitar a transparência referen-
cial. O que acontece dentro da biblioteca é irrelevante.
Uma biblioteca funcional oferece mais opções. Algumas funções podem ser imple-
mentadas em estilo funcional e outras em estilo imperativo; Não importa. Dividir
uma lista vinculada individualmente ou localizar um elemento por seu índice é
muito mais fácil e rápido quando implementado de forma imperativa em vez de
funcional. Isso ocorre porque a lista encadeada individualmente não é adaptada
para tal operação.
Umcaso de uso comum para listas é descobrir se uma lista está contida em outra
lista (mais longa). Você deseja saber se uma lista é uma sublista de outra lista.
Exercício 8.16
Dica
Solução
Vocêpode desenvolver muitas outras funções úteis para trabalhar com listas. Os
exercícios a seguir fornecem alguma prática neste domínio. As soluções propostas
certamente não são as únicas. Sinta-se livre para inventar o seu próprio.
Exercício 8.17
o código a seguir deve criar Map pares contendo (chave, valor), onde cada chave é
um nome e o valor correspondente é a lista de Payment instâncias feitas pela pes-
soa correspondente:
Use um Kotlin imutável Map<B, List<A>> . Para agregar valor, você precisa ve-
rificar se a chave correspondente já está no mapa. Se for encontrado, você adici-
ona o valor à lista correspondente a esta chave. Caso contrário, você cria uma
nova ligação da chave para uma lista de singleton contendo o valor a ser adicio-
nado. Isso pode ser feito facilmente com a getOrDefault funçãodo Map classe.
Solução
Aqui está uma versão imperativa. Não há muito a dizer sobre isso, pois é um có-
digo imperativo tradicional com um estado local mutável. Se você deseja que a or-
dem dos elementos seja mantida nas sublistas, primeiro é necessário inverter a
lista:
Aqui, você pode ver oOperador Elvis ( ?: ) em ação porque este exemplo usa um
mapa imutável Kotlin que retorna um tipo anulável. Usar tipos anuláveis interna-
mente é perfeitamente aceitável para uma programação segura, desde que você
não permita que esses tipos vazem de suas funções. Aqui eles fazem.
Uma solução melhor seria usar um mapa retornando Result.Empty em vez de
null quando a chave não for encontrada. Você também pode usar a getOrDe-
fault função:
Uma solução mais idiomática é usar a let funçãoda biblioteca padrão Kotlin:
Mas isso não ajuda porque resolve o problema na função, onde não é necessário,
e não fora, onde o chamador ainda pode usar o acesso normal retornando null .
No capítulo 11, você aprenderá como criar seu próprio imutável Map que resolve
esse problema.
Inverter a lista primeiro leva algum tempo, então você pode preferir usar use
foldRight em vez de foldLeft . Há um risco potencial de explodir a pilha, no
entanto. Aqui está a solução com foldRight :
Exercício 8.18
Escreva uma unfold função que pegue um elemento inicial S e uma função f de
S a Option<Pair<A, S>> , e produza List<A> aplicando sucessivamente f ao
S valor desde que o resultado seja a Some . O código a seguir deve produzir uma
lista de inteiros de 0 a 9:
desdobrar(0) { i ->
se (i < 10)
Opção(Par(i, i + 1))
senão
Opção()
}
Solução
Infelizmente, embora essa solução seja inteligente, ela explodirá a pilha em pouco
mais de 1.000 etapas de recursão. Para resolver esse problema, você pode criar
uma versão corecursiva:
fun <A, S> desdobrar(z: S, getNext: (S) -> Opção<Par<A, S>>): List<A> {
tailrec fun desdobrar(acc: List<A>, z: S): List<A> {
val proximo = getPróximo(z)
retornar quando (próximo) {
Option.None -> acc
é Option.Some ->
desdobrar(acc.cons(próximo.valor.primeiro), próximo.valor.segundo)
}
}
return desdobrar(Lista.Nil, z).reverse()
}
O problema com esta versão corecursiva é que você precisa inverter o resultado.
Isso pode não ser importante para listas pequenas, mas pode se tornar irritante se
o número de elementos aumentar demais.
Aqui, a unfold função gera valores até que a next função retorne None . Se você
precisar usar uma função que pode produzir um erro, você pode usar a Re-
sult classe em vez de Option :
Exercício 8.19
Escreva uma range função que receba dois inteiros como parâmetros e produza
uma lista de todos os inteiros maiores ou iguais ao primeiro e menores que o
segundo.
Dica
Solução
Exercício 8.20
Crie uma exists função que receba uma função de A para que Boolean repre-
sente uma condição e que retorne true se a lista contiver pelo menos um ele-
mento que satisfaça essa condição. Não use recursão explícita; construir sobre as
funções que você já definiu.
Dica
Solução
Mas esta é uma função baseada em pilha recursiva não caudal e explodirá a pilha
se a lista for longa e se nenhum elemento satisfatório for encontrado nos primei-
ros 1.000 ou mais elementos. Aliás, também lançará uma exceção se a lista estiver
vazia, então você teria que definir uma função abstrata na List classecom uma
implementação específica para a Nil subclasse. Uma solução muito melhor con-
siste em reutilizar a foldLeft funçãocom um parâmetro zero:
Exercício 8.21
Crie uma forAll função que receba uma função de A para Boolean que repre-
sente uma condição e que retorne true se todos os elementos da lista satisfize-
rem essa condição.
Dica
Não use recursão explícita. E mais uma vez, você nem sempre precisa avaliar a
condição de todos os elementos da lista. A forAll função será semelhante aa
exists função.
Solução
Esta função verifica se existe um elemento que não atende ao inverso dea
condição.
A maioriacálculos que são aplicados a listas recorrem a dobras. Uma dobra en-
volve a aplicação de uma operação quantas vezes houver elementos na lista. Para
listas longas e operações de longa duração, uma dobra pode levar um tempo con-
siderável. Como a maioria dos computadores agora está equipada com processa-
dores multicore (se não múltiplos processadores), você pode ficar tentado a en-
contrar uma maneira de fazer o computador processar uma lista em paralelo.
Para paralelizar uma dobra, você precisa apenas de uma coisa (além de um pro-
cessador multicore, é claro): uma operação adicional que permite recompor os re-
sultados de todas as computações paralelas.
Levao exemplo de uma lista de inteiros. Você não pode paralelizar diretamente
encontrando a média de todos os números inteiros. Você pode dividir a lista em
quatro partes (se tiver um computador com quatro processadores) e calcular a
média de cada sublista. Mas você não seria capaz de calcular a média da lista a
partir das médias das sublistas.
Por outro lado, calcular a média de uma lista implica calcular a soma de todos os
elementos e depois dividi-la pelo número de elementos. E calcular a soma é algo
que pode ser facilmente paralelizado calculando as somas das sublistas e, em se-
guida, calculando a soma das somas das sublistas.
Este é um exemplo bem particular, onde a operação utilizada para a dobra (adi-
ção) é a mesma que a operação utilizada para montar os resultados da sublista.
Este nem sempre é o caso. Veja o exemplo de uma lista de caracteres dobrada adi-
cionando caracteres individuais a um arquivo String . Para montar os resulta-
dos intermediários, você precisa de uma operação diferente: stringconcatenação.
Exercício 8.22
Escreva uma divide(depth: Int) função que divida uma lista em várias su-
blistas. A lista será dividida em duas, e cada sublista recursivamente dividida em
duas, com o depth parâmetro representando o número de etapas de recursão.
Esta função será implementada na List classe pai com a seguinte assinatura:
Dica
Você primeiro definirá uma nova versão da splitAt funçãoque retorna uma
lista de listas em vez de um arquivo Pair<List, List> . Vamos chamar esta
função splitListAt e dê-lhe a seguinte assinatura:
Esta função sempre retorna uma lista de duas listas. Então você pode definir a
divide função da seguinte forma:
O Nil caso da when expressão é código morto porque a função local nunca será
chamada com Nil seu parâmetro. Você poderia então usar uma conversão explí-
cita para Cons . Observe também que você realmente não precisa da palavra-
tailrec chaveporque o número de etapas de recursão será apenas log(length) .
Você nunca terá memória heap suficiente para armazenar uma lista por tanto
tempo que causaria uma pilhatransbordar.
Solução
Primeiro, você deve definir o número de sublistas que deseja usar e dividir a lista
de acordo:
dividir (1024)
Em seguida, você mapeará a lista de sublistas com uma função que enviará uma
tarefa para o ExecutorService . Esta tarefa consiste em dobrar cada sublista e
retornar uma Future instância. A lista de Future instâncias é mapeada para
uma função que chama get cada uma Future para produzir uma lista de resul-
tados (uma para cada sublista). Você deve capturar as possíveis exceções. Eventu-
almente, a lista de resultados é dobrada com a segunda função e o resultado é re-
tornado em um arquivo Success . No caso de uma exceção, um Failure é
retornado:
fun <B> parFoldLeft(es: ExecutorService,
identidade: B,
f: (B) -> (A) -> B,
m: (B) -> (B) -> B): Resultado<B> =
tentar {
resultado val: List<B> = divide(1024).map { list: List<A> ->
es.submit<B> { list.foldLeft(identity, f) }
}.map<B> { fb ->
tentar {
fb.get()
} catch (e: InterruptedException) {
lance RuntimeException(e)
} catch (e: ExecutionException) {
lance RuntimeException(e)
}
}
Resultado(resultado.dobraEsquerda(identidade, m))
} catch (e: Exceção) {
Resultado.falha(e)
}
Exercício 8.24
Embora o mapeamento possa ser implementado por meio de uma dobra (e possa
se beneficiar da paralelização automática), ele também pode ser implementado
em paralelo, sem usar uma dobra. Esta é provavelmente a paralelização automá-
tica mais simples que pode ser implementada em uma lista.
Dica
Não há quase nada para fazer neste exercício. Envie cada aplicativo de função
para o ExecutorService e obtenha os resultados de cada função resultante
correspondente.
Solução
O benchmark disponível no código que acompanha este livro permitirá que você
meça qualquer aumento no desempenho. Este aumento pode variar dependendo
da máquina que executa o programa.
Esta versão paralela map cria uma única tarefa para cada elemento da lista. É
mais eficiente para mapear uma lista curta com uma função que representa uma
computação longa. Com listas longas e cálculos rápidos, o ganho de velocidade
pode não ser tão alto - podeatésernegativo.
Resumo