You are on page 1of 61

8Manipulação avançada de listas

Nissocapítulo

Acelerando o processamento de listas com memoização


Compondo List e Result
Implementando acesso indexado em listas
Desdobramento de listas
Processamento automático de lista paralela

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.

Nos capítulos 6 e 7, você aprendeu como representar dados opcionais e erros.


Neste capítulo, você aprenderá como compor operações que produzem dados op-
cionais ou erros com listas. Você também desenvolveu algumas funções que esta-
vam longe do ideal, como length , e eu disse que acabaria aprendendo técnicas
mais eficientes para essas operações. Neste capítulo, você aprenderá como imple-
mentar essas técnicas mais eficientes. Você também aprenderá como paralelizar
automaticamente algumas operações de lista para se beneficiar da arquitetura
multicore dos computadores atuais.

8.1O problema do comprimento

Dobrandouma lista envolve começar com um valor e compô-lo sucessivamente


com cada elemento da lista. Obviamente, isso leva um tempo proporcional ao ta-
manho da lista. Existe alguma maneira de tornar essa operação mais rápida? Ou,
pelo menos, existe uma maneira de fazê-lo aparecer mais rápido? Como exemplo
de aplicação de dobra, você criou uma length funçãono List capítulo 5 (exercí-
cio 5.10) com a seguinte implementação:

fun length(): Int = foldLeft(0) { { _ -> it + 1} }

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:

fun sum(lista: List<Int>): Int = list.foldRight(0) { x -> { y -> x + y } }

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.

Algumas operações podem depender de algumas características do tipo do ele-


mento, mas não do tipo específico em si. Por exemplo, uma max função que re-
torna o elemento máximo de uma lista precisará apenas de um Comparator ou o
tipo seja Comparable . E uma função mais genérica sum poderia ser definida
para tipos que implementam uma Summable interface que define um plus fun-
ção.

8.2 O problema de desempenho

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.

8.3 Os benefícios da memoização

mantendouma contagem dos elementos na lista levará algum tempo, portanto,


adicionar um elemento a uma lista será um pouco mais lento do que se você não
mantivesse a contagem. Pode parecer que você está trocando tempo contra
tempo. Se você criar uma lista de um milhão de elementos, perderá um milhão de
vezes o tempo necessário para adicionar um à contagem. Em compensação, no
entanto, o tempo necessário para obter o comprimento da lista será próximo de 0
(e obviamente constante). Talvez o tempo total perdido em incrementar a conta-
gem seja igual ao ganho ao chamar length . Mas assim que você paga
length mais de uma vez, o ganho é absolutamente óbvio.

8.3.1 Lidando com as desvantagens da memoização

Memorizaçãotem algumas desvantagens. Nesta seção, descrevo essas desvanta-


gens e forneço algumas orientações sobre como escolher se devo usar a
memoização.
A memoização pode transformar uma função que funciona em tempo O( n )
(tempo proporcional ao número de elementos) em tempo O(1) (tempo constante).
Este é um grande benefício, embora tenha um custo de tempo porque torna a in-
serção de elementos um pouco mais longa. Mas retardar a inserção geralmente
não é um grande problema. Um problema muito mais importante é o aumento do
espaço de memória.

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é

abstract fun lengthMemoized(): Int

Solução

A implementação na Nil classe retorna 0:

substitui comprimento divertidoMemoized(): Int = 0

Para implementar a Cons versão, você deve primeiro adicionar o campo memoi-
zing à classe:

classe interna Cons<out A>(cabeçalho val interno: A,


cauda val interna: List<A>): List<A>() {
comprimento do val privado: Int = tail.lengthMemoized() + 1

...

Então você pode implementar a lengthMemoized função para retornar o


comprimento:

sobrescrever fun lengthMemoized() = length

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é

comprimento do val abstrato: Int

No Nil objeto, a propriedade é definida como 0:

substituir val comprimento = 0


E na Cons classe, ela é inicializada exatamente como na implementação da fun-
ção anterior:

substituir val comprimento = cauda.comprimento + 1

Memorizar o valor máximo ou mínimo em uma lista de Comparable poderia ser


feito da mesma forma, mas não ajudaria no caso em que você deseja remover o
valor max ou min da lista. Elementos mínimos ou máximos são frequentemente
acessados ​para recuperar elementos por prioridade. Nesse caso, a compare-
To função dos elementoscompararia suas prioridades.

Memorizar a prioridade permitirá que você saiba imediatamente qual elemento


tem a prioridade máxima, mas não ajudaria muito porque o que você geralmente
precisa é remover o elemento correspondente. Para esses casos de uso, você preci-
sará de uma estrutura de dados diferente, que aprenderá a criar emcapítulo 11.

8.3.2 Avaliando melhorias de desempenho

ComoEu disse, cabe a você decidir se deve memorizar algumas funções da


List classe. Algumas experiências devem ajudá-lo a tomar sua decisão. Medir o
tamanho da memória disponível antes e depois da criação de uma lista de um mi-
lhão de inteiros mostra um pequeno aumento ao usar a memoização.

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.

8.4 Lista e Composição de Resultados

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.

8.4.1 Manipulando listas retornando Result

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

Implemente uma headSafe função List<A> que retornará um arquivo


Result<A> .

Dica

Use a seguinte declaração de função abstrata List e implemente-a em cada


subclasse:

abstract fun headSafe(): Resultado<A>

Solução

A implementação da Nil classe retorna um vazio Result :

override fun headSafe(): Result<Nothing> = Result()

A Cons implementação retorna Success segurando o valor principal:

override fun headSafe(): Result<A> = Result(head)

Exercício 8.3

Criar uma lastSafe funçãoretornando um Result do último elemento da lista.


Dica

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:

fun lastSafe(): Resultado<A> = quando (este) {


Nil -> Resultado()
é Contras -> quando (cauda) {
Nil -> Resultado(cabeça)
é Contras -> tail.lastSafe()
}
}

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:

tailrec fun <A> lastSafe(list: List<A>): Result<A> = when (list) {


Lista.Nil -> Resultado()
é List.Cons<A> -> quando (list.tail) {
List.Nil -> Resultado(list.head)
é List.Cons -> lastSafe(list.tail)
}
}

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:

{ _: Resultado<A> -> { y: A -> Resultado(y) } }

Então você precisa foldLeft da lista usando Result() como identidade:

fun lastSafe(): Resultado<A> =


foldLeft(Result()) { _: Result<A> -> { y: A -> Result(y) } }

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

É possível criar tal implementação:

fun headSafe(): Resultado<A> =


foldRight(Result()) { x: A -> { _: Result<A> -> Result(x) } }
O único benefício é que é mais divertido se você gostar desse jeito. Ao planejar a
lastSafe implementação, você sabia que precisava percorrer a lista para encon-
trar o último elemento. Para encontrar o primeiro elemento, você não precisa per-
correr a lista.

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:

fun headSafe(): Resultado<A> = quando (este) {


Nil -> Resultado()
é Contras -> Resultado(cabeça)
}

8.4.2 Conversão de List<Result> para Result<List>

Quandouma lista contém os resultados de alguns cálculos, geralmente será um ar-


quivo List<Result> . Por exemplo, mapear uma função de T para
Result<U> em uma lista de T produz um arquivo List<Result<U>> . Esses va-
lores geralmente terão que ser compostos com funções que usam a
List<T> como argumento. Isso significa que você precisará converter o resul-
tado List<Result<U>> em um List<U> , que é o mesmo tipo de nivelamento
envolvido na flatMap função. A grande diferença é que dois tipos de dados dife-
rentes estão envolvidos: List e Result . Você pode aplicar várias estratégias
para essa conversão:

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.

A primeira solução corresponderia a uma lista de resultados onde todos os resul-


tados são opcionais. A segunda solução significa que deve haver pelo menos um
sucesso na lista para que o resultado seja um sucesso. E a terceira solução corres-
ponde ao caso em que todos os resultados são obrigatórios.

Exercício 8.5

Escreva uma função chamada flattenResult que recebe a


List<Result<A>> como argumento e retorna a List<A> contendo todos os va-
lores de sucesso da lista original, ignorando as falhas e os valores vazios. Esta será
uma função de nível de pacote com a seguinte assinatura:

fun <A> flattenResult(list: List<Result<A>>): List<A>


Tente não usar recursão explícita, mas compor funções das classes List e . Re-
sult

Dica

O nome escolhido para a função é uma indicação do que você precisa fazer.

Solução

Para resolver este exercício, você pode começar transformando cada


Result<A> elementoda lista em a List<A> (se for um sucesso) ou em uma lista
vazia (em caso de falha) usando a seguinte função:

{ ra -> ra.map { List(it) }.getOrElse(List()) }

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:

fun <A> flattenResult(list: List<Result<A>>): List<A> =


list.flatMap { ra -> ra.map { List(it) }.getOrElse(List()) }

Exercício 8.6

Escreva uma sequence função que combine a List<Result<A>> em a


Result<List<A>> . Será um Success<List<A>> se todos os valores na lista ori-
ginal forem Success instâncias ou Failure<List<A>> caso contrário. Aqui está
sua assinatura:
fun <A> sequence(list: List<Result<A>>): Result<List<A>>

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

Aqui está a implementação usando foldRight e Result.map2 :

import com.fpinkotlin.common.map2

...

fun <A> sequence(list: List<Result<A>>): Result<List<A>> =


list.foldRight(Result(List())) { x ->
{ y: Resultado<Lista<A>> ->
map2(x, y) { a -> { b: List<A> -> b.cons(a) } }
}
}

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.

Essa implementação trata um vazio Result como se fosse um Failure e retorna


o primeiro caso de falha encontrado, que pode ser a Failure ou an Empty . Isso
pode ou não ser o que você precisa.
Para manter a ideia de que Empty significa dados opcionais, você precisa pri-
meiro filtrar a lista para remover os Empty elementos. Mas para isso, você preci-
saria de uma isEmpty função na Result classe retornando true na Empty sub-
classe e false em Success e Failure :

fun <A> sequence2(list: List<Result<A>>): Result<List<A>> =


list.filter{ !it.isEmpty() }.foldRight(Result(List())) { x ->
{ y: Resultado<Lista<A>> ->
map2(x, y) { a -> { b: List<A> -> b.cons(a) } }
}
}

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:

fun <A, B> traverse(lista: Lista<A>, f: (A) -> Resultado<B>): Resultado<Lista<B>>

Em seguida, defina uma nova versão de sequence em termos de traverse .

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

Primeiro defina a traverse função:

fun <A, B> percorrer(lista: Lista<A>, f: (A) -> Resultado<B>): Resultado<Lista<B>> =


list.foldRight(Result(List())) { x ->
{ y: Resultado<Lista<B>> ->
map2(f(x), y) { a -> { b: List<B> -> b.cons(a) } }
}
}

Então você pode redefinir a sequence função em termosde traverse :

fun <A> sequence(list: List<Result<A>>): Result<List<A>> =


traverse(lista, { x: Resultado<A> -> x })

8.5 Abstrações da Lista Comum

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:

Compactando e descompactando listas


Transformando uma lista de pares em um par de listas
Transformando uma lista de qualquer tipo em um par de listas
8.5.1Compactando e descompactando listas

Fechando é o processo de reunir duas listas em uma, combinando os elementos


com o mesmo índice. Descompactar é o procedimento inverso, consistindo em fa-
zer duas listas de uma desconstruindo os elementos, como produzir duas listas
x e y coordenadas de uma lista de pontos.

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:

fun <A, B, C> zipWith(lista1: Lista<A>,


lista2: Lista<B>,
f: (A) -> (B) -> C): Lista<C>

Esta função pega a List<A> e a List<B> e produz a List<C> com a ajuda de


uma função de A para B para C .

Dica

A compactação deve ser limitada ao comprimento da lista mais curta.

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:

fun <A, B, C> zipWith(lista1: Lista<A>,


lista2: Lista<B>,
f: (A) -> (B) -> C): Lista<C> {
tailrec
fun zipWith(acc: List<C>,
lista1: Lista<A>,
lista2: Lista<B>): Lista<C> = quando (lista1) {
List.Nil -> acc
é List.Cons -> when (list2) {
List.Nil -> acc
é List.Cons ->
zipWith(acc.cons(f(lista1.head)(lista2.head)),
lista1.cauda, ​
lista2.cauda)
}
}
return zipWith(Lista(), lista1, lista2).reverse()
}

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

O exercício anterior consistia em criar uma lista combinando elementos de ambas


as listas por seus índices. Escreva uma product funçãoque produz uma lista de
todas as combinações possíveis de elementos retirados de ambas as listas. Dadas
as duas listas list("a","b", "c") e list("d", "e", "f") , e a concatenação
de strings, o produto das duas listas deve ser List("ad", "ae", "af", "bd",
"be", "bf", "cd", "ce", "cf") .

Dica

Para este exercício, você não precisa usar recursão explícita.

Solução

A solução é semelhante ao padrão de compreensão que você compôs Result no


capítulo 7. A única diferença aqui é que ele produz tantas combinações quanto o
produto do número de elementos nas listas, embora para combinar Result , o
número de combinações sempre foi limitado para um:

fun <A, B, C> produto(lista1: Lista<A>,


lista2: Lista<B>,
f: (A) -> (B) -> C): Lista<C> =
list1.flatMap { a -> list2.map { b -> f(a)(b) } }
NOTA É possível compor mais de duas listas desta forma. O único problema é que
o número de combinações crescerá exponencialmente.

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:

produto(Lista(1, 2), Lista(4, 5, 6)) { x -> { y: Int -> Par(x, y) } }


zipWith(List(1, 2), List(4, 5, 6)) { x -> { y: Int -> Pair(x, y) } }

A primeira expressão produz uma lista de todos os pares possíveis construídos a


partir dos elementos de ambas as listas:

[(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), NIL]

A segunda expressão produz apenas a lista de pares construída a partir de ele-


mentos com os mesmos índices:

[(1, 4), (2, 5), 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:

fun <A, B> unzip(list: List<Pair<A, B>>): Pair<List<A>, List<B>>


Dica

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:

fun <A, B> unzip(list: List<Pair<A, B>>): Pair<List<A>, List<B>> =


list.coFoldRight(Pair(List(), List())) { par ->
{ listPair: Pair<Lista<A>, Lista<B>> ->
Pair(listPair.first.cons(pair.first),
listPair.second.cons(pair.second))
}
}

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

A solução é praticamente a mesma do exercício 8.10.

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

fun <A, B> unzip(list: List<Pair<A, B>>): Pair<List<A>, List<B>> =


list.unzip { it }

8.5.2 Acessando elementos por seu índice

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

Escreva uma getAt funçãoque recebe um índice como argumento e retorna o


elemento correspondente. A função não deve lançar uma exceção caso o índice
esteja fora dos limites.
Dica

Desta vez, comece com uma versão explicitamente recursiva. Em seguida, tente
responder às seguintes perguntas:

É possível fazer com dobra? Direita ou esquerda?


Por que a versão recursiva explícita é melhor?
Você consegue pensar em uma implementação melhor?

Lembrete: você encontrou a recursão de cauda pela primeira vez no capítulo 4, e


os capítulos 3 e 5 aprofundaram o dobramento.

Solução

A solução explicitamente recursiva é fácil:

fun getAt(índice: Int): Resultado<A> {


tailrec fun <A> getAt(lista: Lista<A>, índice: Int): Resultado<A> =
quando (lista) {
Nil -> Result.failure("Código morto. Nunca deve ser executado.")
é Contras ->
se (índice == 0)
Resultado(lista.head)
senão
getAt(lista.cauda, ​
índice - 1)
}
return if (índice < 0 || índice >= comprimento())
Result.failure("Índice fora do limite")
senão
getAt(este, índice)
}

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:

fun getAt(índice: Int): Resultado<A> {


tailrec fun <A> getAt(lista: Cons<A>, índice: Int): Resultado<A> =
se (índice == 0)
Resultado(lista.head)
senão
getAt(list.tail as Cons, index - 1)

return if (índice < 0 || índice >= comprimento())


Result.failure("Índice fora do limite")
senão
getAt(isto como Contras, índice)
}

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)
}

return if (índice < 0 || índice >= comprimento())


Result.failure("Índice fora do limite")
senão
getAt(este, índice)
}

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:

fun getAtViaFoldLeft(index: Int): Resultado<A> =


Pair(Result.failure<A>("Índice fora do limite"), index).let {
if (índice < 0 || índice >= comprimento())
isto
senão
foldLeft(it) { ta ->
{a->
if (ta.second < 0)
ta
senão
Par(Resultado(a), ta.segundo - 1)
}
}
}.primeiro

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.

Exercício 8.13 (difícil)

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

Por analogia, o elemento absorvedor de qualquer operação às vezes é chamado


de zero , mas lembre-se que nem sempre é igual a 0. O valor 0 é apenas o ele-
mento absorvente para a multiplicação. Para a adição de números inteiros positi-
vos, seria infinito.

A Nil implementação retorna o identity parâmetro:

substituir fun <B> foldLeft(identidade: B,


zero: B, f: (B) -> (Nada) -> B): B = identidade

E aqui está a Cons implementação:

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.

O 0 valor é a Pair<Result<A>, Int> com o Int valor igual a -1 (o primeiro


valor menor que 0 ). Você pode usar um padrão Pair para isso? Não, não pode,
pois deve ter uma função equals especial que retorna true quando os valores in-
teiros são iguais, seja qual Result<A> for. A função completa é a seguinte:

fun getAt(índice: Int): Resultado<A> {


data class Pair<out A>(val primeiro: Result<A>, val segundo: Int) {
substituir diversão é igual a(outro: Qualquer?): Booleano = quando {
outro == nulo -> falso
outro.javaClass == this.javaClass ->
(outro como Par<A>).segundo == segundo
senão -> falso
}
substituir fun hashCode(): Int =
primeiro.hashCode() + segundo.hashCode()
}

return Pair<A>(Result.failure("Índice fora do limite"), índice)


.let { identidade ->
Pair<A>(Result.failure("Índice fora do limite"), -1).let { zero ->
if (índice < 0 || índice >= comprimento())
identidade
senão
foldLeft(identity, zero) { ta: Pair<A> ->
{ a: A ->
if (ta.second < 0)
ta
senão
Par(Resultado(a), ta.segundo - 1)
}
}
}
}.primeiro
}

Agora a dobra para automaticamente assim que o elemento procurado é encon-


trado. Você pode usar a nova foldLeft funçãopara escapar de qualquer cálculo
com um elemento zero. (Lembre-se: zero não 0.) Mas, em vez de usar um ele-
mento zero, você pode usar um predicado e fazer a função retornar quando esse
predicado retornar true :

fun abstract <B> foldLeft(acc: B, p: (B) -> Boolean, f: (B) -> (A) -> B): B
A Nil implementação retorna identity como anteriormente:

substituir fun <B> foldLeft(identidade: B,


p: (B) -> Booleano,
f: (B) -> (Nada) -> B): B = identidade

A Cons implementação é semelhante às anteriores, mas com uma pequena dife-


rença. Em vez de testar se acc é igual a zero , você aplica o predicado a acc :

substituir fun <B> foldLeft(identidade: B,


p: (B) -> Booleano,
f: (B) -> (A) -> B): B {
fun <B> foldLeft(acc: B,
p: (B) -> Booleano,
lista: Lista<A>): B = quando (lista) {
Nil -> acc
é Contras ->
se (p(acc))
acc
senão
foldLeft(f(acc)(list.head), p, list.tail, f)
}
return foldLeft(identity, p, this)
}

E aqui está a getAt implementação usando esta versão de foldLeft :

fun getAt(índice: Int): Resultado<A> {


val p: (Pair<Result<A>, Int>) -> Boolean = { it.second < 0 }
return Pair<Result<A>, Int>(Result.failure("Índice fora do limite"), index)
.let { identidade ->
if (índice < 0 || índice >= comprimento())
identidade
senão
foldLeft(identidade, p) { ta: Pair<Resultado<A>, Int> ->
{ a: A ->
se (p(ta))
ta
senão
Par(Resultado(a), ta.segundo - 1)
}
}

}.primeiro
}

8.5.3 Dividindo listas

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

Torne a função explicitamente recursiva.

Solução

Uma solução explicitamente recursiva é fácil de projetar:

fun splitAt(index: Int): Pair<List<A>, List<A>> {


tailrec fun splitAt(acc: List<A>,
lista: Lista<A>, i: Int): Pair<Lista<A>, Lista<A>> =
quando (lista) {
Nil -> Pair(list.reverse(), acc)
é Contras -> se (i == 0)
Par(lista.reverso(), acc)
senão
splitAt(acc.cons(list.head), list.tail, i - 1)
}
retornar quando {
índice < 0 -> splitAt(0)
índice > comprimento () -> splitAt (comprimento ())
senão ->
splitAt(Nil, this.reverse(), this.length() - índice)
}
}
A função principal usa recursão para ajustar o valor do índice, embora esta fun-
ção seja recursiva no máximo uma vez. A função auxiliar é semelhante à ge-
tAt funçãocom a diferença de que a lista é primeiro invertida. A função acumula
os elementos até que a posição do índice seja alcançada, então a lista acumulada
está na ordem correta, mas a lista restante deve ser invertida.

Exercício 8.15 (não tão difícil se você fez o exercício 8.13)

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

Uma solução que percorra toda a lista poderia ser a seguinte:

fun splitAt(index: Int): Pair<List<A>, List<A>> {


val ii = se (índice < 0) 0
else if (index >= length()) length() else index
val identidade = Triplo(Nil, Nil, ii)
val rt = foldLeft(identidade) { ta: Triplo<Lista<A>, Lista<A>, Int> ->
{ a: A ->
if (ta.terceiro == 0)
Triplo(ta.primeiro, ta.segundo.cons(a), ta.terceiro)
senão
Triplo(ta.primeiro.cons(a), ta.segundo, ta.terceiro - 1)
}
}
return Pair(rt.first.reverse(), rt.second.reverse())
}

O resultado da dobra é acumulado no primeiro acumulador de lista até que o ín-


dice seja alcançado (após o valor do índice ter sido ajustado para evitar uma con-
dição de índice fora dos limites. Uma vez que o índice é encontrado, a travessia da
lista continua, mas os valores restantes são acumulado no segundo acumulador
de lista.

Um problema com essa implementação é que ao acumular os valores restantes no


segundo acumulador de lista, você está invertendo essa parte da lista. Não apenas
você não precisa percorrer o restante da lista, mas isso é feito duas vezes aqui:
uma vez para acumular na ordem inversa e outra para eventualmente reverter o
resultado.

Para evitar este problema, modifique a versão especial de escape de fol-


dLeft para que ele retorne não apenas o resultado de escape (o elemento absor-
vente ou zero), mas também o restante da lista, intocada. Para conseguir isso,
você deve alterar a assinatura para retornar um Pair :

fun abstract <B> foldLeft(identity: B, zero: B,


f: (B) -> (A) -> B): Par<B, Lista<A>>
Então você precisa alterar a implementação na Nil classe:

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)
}

Agora você pode reescrever a splitAt funçãousando esta foldLeft função


especial:

fun splitAt(index: Int): Pair<List<A>, List<A>> {

data class Pair<out A>(val primeiro: List<A>, val segundo: Int) {


substituir diversão é igual a(outro: Qualquer?): Booleano = quando {
outro == nulo -> falso
outro.javaClass == this.javaClass ->
(outro como Par<A>).segundo == segundo
senão -> falso
}

substituir fun hashCode(): Int =


primeiro.hashCode() + segundo.hashCode()
}

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)
}
}
}

Aqui, novamente, você precisa de uma Pair classe específica com


um equals função que retorna true quando os segundos elementos são iguais,
não levando em conta o primeiro elemento. Observe que a segunda lista resul-
tante não precisa serinvertida.
Quando não usar dobras

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 em um ambiente orientado a imperativos, como a JVM,


pode ser comparada a um compilador para uma linguagem orientada a funcional.
O código compilado será sempre baseado em áreas de memória e registros mutá-
veis ​porque é isso que o computador entende.

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.

A maneira mais funcional de ir provavelmente não é implementar funções basea-


das em índices baseados em dobras, mas evitar a implementação dessas funções.
Se você precisar de estruturas com essas funções, o melhor a fazer é criar estrutu-
ras específicas, como você verá no capítulo 10.
8.5.4 Procurando por sublistas

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

Implementar uma hasSubList funçãopara verificar se uma lista é uma sublista


de outra. Por exemplo, a lista (3, 4, 5) é uma sublista de (1, 2, 3, 4, 5), mas não de
(1, 2, 4, 5, 6). Implemente esta função com a seguinte assinatura:

fun hasSubList(sub: List<@UnsafeVariance A>): Booleano

Dica

Você primeiro terá que implementar uma startsWith funçãopara determinar se


uma lista começa com uma sublista. Feito isso, você testará essa função recursiva-
mente, começando por cada elemento da lista.

Solução

startsWith Uma função explicitamente recursivapode ser implementado na


List classe, não esquecendo de desabilitar a verificação de variância no
parâmetro:

fun startsWith(sub: List<@UnsafeVariance A>): Boolean {


tailrec fun startsWith(list: List<A>, sub: List<A>): Boolean =
quando (sub) {
nada -> verdadeiro
é Contras -> quando (lista) {
Nil -> falso
é Contras -> if (list.head == sub.head)
começaCom(lista.cauda, ​
sub.cauda)
senão
falso
}
}
return começa Com(este, sub)
}

A partir daí, a implementação hasSubList épara a frente:

fun hasSubList(sub: List<@UnsafeVariance A>): Boolean {


tailrec
fun <A> hasSubList(list: List<A>, sub: List<A>): Boolean =
quando (lista) {
Nil -> sub.isEmpty()
é Contras ->
if (list.startsWith(sub))
verdadeiro
senão
hasSubList(list.tail, sub)
}
return hasSubList(this, sub)
}
8.5.5 Funções diversas para trabalhar com listas

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

Criar uma groupBy funçãocom as seguintes características:

Leva uma função de A a B como parâmetro.


Ele retorna um Map, onde as chaves são o resultado da função aplicada a cada
elemento da lista e os valores são listas de elementos correspondentes a cada
chave.

Por exemplo, dada uma lista de Payment instâncias como

classe de dados Payment(nome do valor: String, valor do valor: Int)

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:

val map: Map<String, List<Payment>> = list.groupBy { x -> x.name }


Dica

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:

fun <B> groupBy(f: (A) -> B): Mapa<B, Lista<A>> =


reverse().foldLeft(mapOf()) { mt: Map<B, List<A>> ->
{t->
val chave = f(t)
mt + (chave para (mt[chave] ?: Nil).cons(t))
}
}

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:

fun <B> groupBy(f: (A) -> B): Mapa<B, Lista<A>> =


reverse().foldLeft(mapOf()) { mt: Map<B, List<A>> ->
{t->
valk = f(t)
mt + (k para (mt.getOrDefault(k, Nil)).cons(t))
}
}

Uma solução mais idiomática é usar a let funçãoda biblioteca padrão Kotlin:

fun <B> groupBy(f: (A) -> B): Mapa<B, Lista<A>> =


reverse().foldLeft(mapOf()) { mt: Map<B, List<A>> ->
{t->
f(t).let {
mt + (para (mt.getOrDefault(it, Nil)).cons(t))
}
}
}

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 :

fun <B> groupBy(f: (A) -> B): Mapa<B, Lista<A>> =


foldRight(mapOf()) { t ->
{ mt: Mapa<B, Lista<A>> ->
f(t).let { mt + (para (mt.getOrDefault(it, Nil)).cons(t)) }
}
}

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

Uma versão recursiva simples e não segura de pilha é fácil de implementar:


fun <A, S> desdobra_(z: S, f: (S) -> Opção<Par<A, S>>): List<A> =
f(z).map({ x ->
desdobra_(x.segundo, f).cons(x.primeiro)
}).getOrElse(List.Nil)

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.

Esta implementação requer que a Option classeestar no mesmo módulo que a


List turma. Um módulo é um dos seguintes:

Um módulo IntelliJ IDEA


Um projeto Maven
Um conjunto de origem Gradle
Um conjunto de arquivos compilados com uma invocação de uma tarefa Ant

Para este exercício, a Option turmafoi copiado do common móduloao advance-


dlisthandling módulo. Em uma situação real, seria o inverso, a List classees-
tar no common módulo.

Aqui está um exemplo de uso unfold :

fun main(args: Array<String>) {


val f: (Int) -> Opção<Pair<Int, Int>> =
{isso ->
if (it < 10_000) Option(Pair(it, it + 1)) else Option()
}
resultado val = desdobrar(0, f)
println(resultado)
}

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 :

fun <A, S> desdobrar(z: S,


getNext: (S) -> Result<Pair<A, S>>): Result<List<A>> {
tailrec fun desdobrar(acc: List<A>, z: S): Result<List<A>> {
val proximo = getPróximo(z)
retornar quando (próximo) {
Result.Empty -> Result(acc)
é Result.Failure -> Result.failure(next.exception)
é Result.Success ->
desdobrar(acc.cons(próximo.valor.primeiro), próximo.valor.segundo)
}
}
return desdobrar(Lista.Nil, z).map(Lista<A>::reverso)
}

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

Você deve usar as funções que já definiu.

Solução

Isso é simples se você reutilizar a função do exercício 8.18:

fun range(start: Int, end: Int): List<Int> {


return desdobrar(iniciar) { i ->
se (i < fim)
Opção(Par(i, i + 1))
senão
Opçã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

Não há necessidade de avaliar a condição de todos os elementos da lista. A função


deve retornar assim que o primeiro elemento que satisfaça a condição for
encontrado.

Solução

Uma solução recursiva pode ser definida da seguinte forma:

divertido existe(p:(A) -> Booleano): Booleano =


quando isso) {
Nil -> falso
é Cons -> p(cabeça) || cauda.existe(p)
}

Porque o || operadoravalia seu segundo argumento preguiçosamente, o pro-


cesso recursivo para assim que um elemento é encontrado que satisfaça a condi-
ção expressa pelo predicado p .

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:

fun existe(p: (A) -> Boolean): Boolean =


foldLeft(false, true) { x -> { y: A -> x || p(y) } }.primeiro

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

A solução está próxima da exists função com duas diferenças - os valores de


identidade e zero são invertidos e o Boolean operador é && em vez de || :
fun forAll(p: (A) -> Boolean): Boolean =
foldLeft(true, false) { x -> { y: A -> x && p(y) } }.first

Outra possibilidade é reutilizar a exists função:

fun forAll(p: (A) -> Boolean): Boolean = !exists { !p(it) }

Esta função verifica se existe um elemento que não atende ao inverso dea
condição.

8.6 Processamento paralelo automático de listas

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.

8.6.1 Nem todos os cálculos podem ser paralelizados

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.

8.6.2 Dividindo a lista em sublistas

Primeiro,você deve dividir a lista em sublistas e deve fazer isso automaticamente.


Uma questão importante é quantas sublistas você deve obter. Inicialmente, você
pode pensar que uma sublista para cada processador disponível seria o ideal, mas
não é bem assim. O número de processadores (ou, mais precisamente, o número
de núcleos lógicos) não é o fator mais importante.

Há uma questão muito mais crucial: todos os cálculos de sublista levarão o


mesmo tempo? Provavelmente não, mas isso depende do tipo de computação. Se
você dividisse a lista em quatro sublistas porque decidiu dedicar quatro encadea-
mentos ao processamento paralelo, alguns encadeamentos poderiam terminar ra-
pidamente, embora outros pudessem ter que fazer um cálculo muito mais longo.
Isso arruinaria o benefício da paralelização porque poderia fazer com que a
maior parte da tarefa de computação fosse manipulada por um único thread.

Uma solução muito melhor é dividir a lista em um grande número de sublistas e,


em seguida, enviar cada sublista a um pool de threads. Desta forma, assim que
uma thread termina de processar uma sublista, ela entrega uma nova para pro-
cessar. Sua primeira tarefa é criar uma função que divide uma lista em sublistas.

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:

fun divide(profundidade: Int): List<List<A>>

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:

fun splitListAt(index: Int): List<List<A>>


Solução

A splitListAt função é uma versão ligeiramente modificada da splitAt fun-


ção:

fun splitListAt(index: Int): List<List<A>> {


tailrec fun splitListAt(acc: List<A>,
lista: Lista<A>, i: Int): Lista<Lista<A>> =
quando (lista) {
Nil -> List(list.reverse(), acc)
é Contras -> se (i == 0)
List(list.reverse(), acc)
senão
splitListAt(acc.cons(list.head), list.tail, i - 1)
}
retornar quando {
index < 0 -> splitListAt(0)
índice > comprimento () -> splitListAt (comprimento ())
senão ->
splitListAt(Nil, this.reverse(), this.length() - index)
}
}

Esta função sempre retorna uma lista de duas listas. Então você pode definir a
divide função da seguinte forma:

fun divide(profundidade: Int): List<List<A>> {


tailrec
fun divide(lista: Lista<Lista<A>>, profundidade: Int): Lista<Lista<A>> =
quando (lista) {
Nil -> lista // código morto
é Contras ->
if (list.head.length() < 2 || profundidade < 1)
Lista
senão
divide(lista.flatMap { x ->
x.splitListAt(x.length() / 2)
}, profundidade - 1)
}
retornar se (this.isEmpty())
Lista(este)
senão
divide(Lista(este), profundidade)
}

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.

8.6.3 Processamento de sublistas em paralelo

Paraprocessar as sublistas em paralelo, você precisará de uma versão especial da


função para executar. Esta versão especial terá, como parâmetro adicional,
um ExecutorService configurado com o número de threads que você deseja
usar em paralelo.
Exercício 8.23

Criar uma parFoldLeft funçãoem List<A> que leva os mesmos parâmetros


de foldLeft mais um ExecutorService e uma função de B para B para B e
que retorna um Result<B> . A função adicional será utilizada para montar os re-
sultados das sublistas. Aqui está a assinatura da função:

fun <B> parFoldLeft(es: ExecutorService,


identidade: B,
f: (B) -> (A) -> B,
m: (B) -> (B) -> B): Resultado<B>

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)
}

Você encontrará um exemplo de benchmark dessa função no código que o acom-


panha ( https://github.com/pysaumont/fpinkotlin ). O benchmark consiste em cal-
cular dez vezes o valor de Fibonacci de 35.000 números aleatórios entre 1 e 30
com um algoritmo lento. Aqui está um resultado típico obtido em um computador
de oito núcleos:

Duração serial 1 thread: 140933


Duração paralelo 2 threads: 70502
Duração paralelo 4 threads: 36337
Duração paralelo 8 threads: 20253

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.

Criar uma parMap funçãoque aplicará automaticamente uma determinada fun-


ção a todos os elementos de uma lista em paralelo. Aqui está a assinatura da
função:

fun <B> parMap(es: ExecutorService, g: (A) -> B): Resultado<Lista<B>>

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

Aqui está a solução:

fun <B> parMap(es: ExecutorService, g: (A) -> B): Result<List<B>> =


tentar {
val result = this.map { x ->
es.submit<B> { g(x) }
}.map<B> { fb ->
tentar {
fb.get()
} catch (e: InterruptedException) {
lance RuntimeException(e)
} catch (e: ExecutionException) {
lance RuntimeException(e)
}
}
Resultado(resultado)
} catch (e: Exceção) {
Resultado.falha(e)
}

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

Você pode usar memoização para acelerar o processamento da lista.


Você pode converter uma List das Result instâncias em uma Result of
List .
Você pode montar duas listas compactando-as. Você também pode descompac-
tar listas para produzir um Pair conjunto de listas.
Você pode implementar acesso indexado a elementos de lista usando recursão
explícita.
Você pode implementar uma versão especial de foldLeft para escapar da do-
bra quando um resultado zero é obtido.
Você pode criar listas desdobrando com uma função e uma condição terminal.
As listas podem ser divididas automaticamente, o que permite o processa-
mento automático de sublistas em paralelo.

You might also like