4 Recursion, Corecursion, and Memoization - The Joy of Kotlin

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 73

4Recursão, corecursão e memoização

Neste capítulo

Usando recursão e corecursão


Criando funções recursivas
Usando funções corecursivas (recursivas de cauda)
Implementando memoização

As funções recursivas são um recurso onipresente em muitas linguagens de pro-


gramação, embora raramente sejam usadas em Java. Isso ocorre devido à má im-
plementação dessa funcionalidade. Felizmente, o Kotlin oferece uma implementa-
ção muito melhor, para que você possa usar a recursão amplamente.

Com a recursão, muitos algoritmos são definidos recursivamente. A implementa-


ção desses algoritmos em linguagens não recursivas consiste principalmente em
traduzir algoritmos recursivos em não recursivos. Usar uma linguagem capaz de
lidar com a recursão não apenas simplifica a codificação, mas também permite
escrever programas que refletem a intenção (o algoritmo original). Esses progra-
mas são geralmente muito mais fáceis de ler e entender. E programar é muito
mais sobre ler programas do que escrevê-los, então é importante criar programas
que mostrem claramente o que é feito ao invés de como é feito.
IMPORTANTE Esteja ciente de que,como loops, a recursão geralmente deve ser
abstraída em funções, em vez de usada diretamente.

4.1Correcursão e recursão

Corecursion está compondo etapas de computação usando a saída de uma etapa


como a entrada da próxima, começando com a primeira etapa. A recursão é a
mesma operação, mas começa com a última etapa. Vamos tomar como exemplo
uma lista de caracteres que você deseja unir em uma representação de string.

4.1.1 Implementando corecursion

vamossuponha que você tenha à sua disposição uma append função que recebe
um par de (String, Char) como argumentos e retorna a string do argumento
com o caractere do argumento anexado a ela:

fun append(s: String, c: Char): String = "$s$c"

Você não pode fazer nada com esta função porque não tem string para começar.
Você precisa de um elemento adicional: ocadeia vazia "" . Com esses dois elemen-
tos, você já pode construir o resultado pretendido. A Figura 4.1 mostra o processa-
mento da lista.
Figura 4.1 Transformando correcursivamente uma lista de caracteres em uma string

Esse processo pode ser descrito em código como

fun toString(list: List<Char>, s: String): String =


if (lista.isEmpty())
s
senão
toString(list.subList(1, list.size), append(s, list[0]))

Observe que list[0] retorna o primeiro elemento da lista, que corresponde a


uma função geralmente conhecida pelo nome head . Por outro lado,
list.subList(1, list.size) corresponde a uma função chamada tail , que
retorna o restante da lista. Você poderia abstraí-los em funções separadas, mas te-
ria que lidar com o caso de uma lista vazia. Aqui, isso não vai acontecer porque a
lista é filtrada pela if..else expressão.

Uma solução mais idiomática seria usar as funções drop e first :

fun toString(list: List<Char>, s: String): String =


if (lista.isEmpty())
s
senão
toString(list.drop(1), append(s, list.first()))

O uso do acesso indexado facilita a comparação entre corecursion e recursion. O


único problema com esta implementação é que você tem que chamar o toS-
tring função com a string vazia como um argumento adicional (ou mais precisa-
mente, como uma parte adicional de seu argumento). A solução simples é escre-
ver outra função para isso, aproveitando o fato de Kotlin permitir que você de-
clare funções dentro de funções:

fun toString(list: List<Char>): String {


fun toString(list: List<Char>, s: String): String =
if (lista.isEmpty())
s
senão
toString(list.subList(1, list.size), append(s, list[0]))
return toString(lista, "")
}

Aqui você terá que usar uma função de bloco, ou seja, uma função com chaves,
para a toString função delimitadora. Você também deve indicar explicitamente
o tipo de retorno de ambas as funções por dois motivos:

Para a função delimitadora, as funções de bloco devem ter um tipo de retorno


explícito; caso contrário, Unit é assumido. ( Unit é o equivalente Kotlin de
Java void , ou mais exatamente Void .)
Para a função incluída, você deve indicar o tipo, porque a função está cha-
mando a si mesma. Nesse caso, o Kotlin não pode inferir o tipo de retorno.

Outra solução seria adicionar um segundo parâmetro à função com um padrãova-


lor:

fun toString(list: List<Char>, s: String = ""): String =


if (lista.isEmpty())
s
senão
toString(list.subList(1, list.size), append(s, list[0]))

4.1.2 Implementando recursão

oimplementação anterior só foi possível porque você tinha à sua disposição o ap-
pend função, que acrescenta um caractere a uma string, o que significa adicionar
o caractereaté o final da corda. Agora pense no que você faria se tivesse acesso
apenas à seguinte função:

fun prepend(c: Char, s: String): String = "$c$s"

Você pode começar com o final da lista. Para isso, você pode inverter a lista antes
de iniciar o cálculo ou alterar a implementação para retornar o último elemento
da lista e a lista sem seu último elemento. Aqui está uma implementação possível:

fun toString(list: List<Char>): String {


fun toString(list: List<Char>, s: String): String =
if (lista.isEmpty())
s
senão
toString(lista.subLista(0, lista.tamanho - 1),
prepend(lista[lista.tamanho - 1], s))
return toString(lista, "")
}

Isso funcionaria, no entanto, apenas com listas que oferecem esse tipo de acesso,
como a lista indexada ou a lista duplamente encadeada, também conhecida como
deque (pronuncia -se dek e significafila dupla ). Se você estivesse trabalhando com
listas encadeadas individualmente, não teria escolha a não ser inverter a lista, o
que está longe de ser eficiente.

Como um cenário de pior caso, pegue listas infinitas. Você pode pensar que não
há nada que possa fazer com listas infinitas, mas isso não é verdade. Com Kotlin,
você pode aplicar funções a listas infinitas, que produzem outras listas infinitas.
Você pode compor essas funções para obter o resultado pretendido e, em seguida,
truncar o resultado antes de ser calculado.

Pense neste exemplo: se você quiser calcular a lista dos 100 primeiros primos
(não que isso seja extremamente útil, mas é apenas um exemplo), você teria que
iterar a lista de inteiros (que é infinita), testando que são primos, e parando após
o 100º primo. Neste exemplo, você certamente não poderia começar invertendo a
lista. A solução é usar recursão em vez de corecursão conforme mostrado na fi-
gura 4.2 .
Figura 4.2 Transformando recursivamente uma lista de caracteres em uma string
Como você pode ver na figura, nenhum cálculo pode ser realizado até que a con-
dição terminal (aqui, o último elemento da lista) seja encontrada. Como resultado,
as etapas intermediárias devem ser armazenadas em algum lugar até que possam
ser avaliadas. Esse processo pode ser expresso em código como

fun toString(list: List<Char>): String =


if (lista.isEmpty())
""
senão
prepend(list[0], toString(list.subList(1, list.size)))

Como você verá, o principal problema com esse tipo de código é armazenar as
etapas intermediárias. Isso ocorre porque a JVM usa um espaço de memória
muito limitado paraisto.

4.1.3 Diferenciando funções recursivas e correcursivas

Considerandoo que você aprendeu na escola, você pode pensar que os exemplos
recursivos e corecursivos são, de fato, funções recursivas , ou seja, funções que
chamam a si mesmas. Bem, se você seguir essa definição de recursão, você está
certo. Mas isso não é recursão.

Uma função é recursiva se chama a si mesma como parte de uma computação.


Caso contrário, não é recursão verdadeira. Pelo menos, não é um processo recur-
sivo. Pode ser um corecursivo. Pense em um método que imprime “Hello, World!”
na tela e, eventualmente, chamando-se recursivamente:
divertido olá() {
println("Olá, Mundo!")
olá()
}

Isso não parece mais um loop infinito do que um método recursivo? Embora seja
implementado como um método recursivo, na verdade é um método corecursivo
que pode se traduzir em um loop infinito! A boa notícia é que o Kotlin é capaz de
fazer essa traduçãoautomaticamente.

4.1.4Escolhendo recursão ou corecursão

oO problema da recursão é que todas as linguagens impõem um limite ao número


de etapas recursivas. Em teoria, a principal diferença entre recursão e corecursão
é a seguinte:

Com corecursion, cada etapa pode ser avaliada assim que é encontrada.
Com a recursão, todas as etapas devem ser armazenadas em algum lugar de al-
guma forma. Isso permite atrasar a avaliação até que a condição terminal seja
encontrada. Só então cada etapa anterior pode ser avaliada, na ordem inversa.

A memória necessária para armazenar as etapas recursivas geralmente é severa-


mente limitada e pode facilmente transbordar. Para evitar esse problema, é me-
lhor evitar a recursão e preferir a corecursão.

A distinção entre recursão e correcursão é ilustrada nas figuras 4.3 e 4.4. Esses nú-
meros representam o cálculo da soma de uma lista de números inteiros. A adição
é um pouco especial porque
Figura 4.3 Um cálculo correcursivo

É comutativo, ou seja, a + b = b + a, o que não é o caso da maioria das opera-


ções, como adicionar um caractere a uma string.
Os dois parâmetros, assim como o resultado, são do mesmo tipo. Mais uma vez,
geralmente não é esse o caso.
Você pode pensar que as operações nas figuras podem ser transformadas, por
exemplo, removendo parênteses. Digamos que não seja possível. (Este é apenas
um exemplo para mostrar o que está acontecendo.)

Na figura 4.3 , você pode ver um processo correcursivo. Cada etapa é avaliada as-
sim que é encontrada. Como resultado, a quantidade de memória necessária para
todo o processo éconstante, simbolizada pelo retângulo na parte inferior da fi-
gura. A Figura 4.4 , por outro lado, mostra como um processo recursivo pode ser
usado para calcular o mesmo resultado.
Figura 4.4 Uma computação recursiva

Nenhum resultado intermediário pode ser avaliado até que todas as etapas sejam
desenvolvidas. Como consequência, a memória necessária para a computação re-
cursiva é muito maior; as etapas intermediárias devem ser empilhadas em algum
lugar antes de serem processadas na ordem inversa.
Usar mais memória não é o pior problema da recursão. O que torna o problema
ainda pior é que as linguagens de computador usam a pilha para armazenar eta-
pas de computação. Isso é inteligente porque as etapas de computação devem ser
avaliadas na ordem inversa em comparação com a ordem em que foram empilha-
das. Infelizmente, o tamanho da pilha é limitado, portanto, se houver muitas eta-
pas, a pilha transbordará, travando o encadeamento de computação.

Quantas etapas de computação podem ser colocadas com segurança na pilha de-
pende do idioma e pode ser configurada. Em Kotlin, é cerca de 20.000; em Java, é
cerca de 3.000. Configurar o tamanho da pilha para um valor maior pode não ser
uma boa ideia porque o mesmo tamanho de pilha (mas não a mesma área de pi-
lha) é usado por todos os encadeamentos. E um tamanho de pilha maior geral-
mente é um desperdício de memória, visto que os processos não recursivos usam
pouco espaço na pilha.

Como você pode ver nas duas figuras, a memória necessária para o processo core-
cursivo é constante. Ele não aumentará se o número de etapas de computação au-
mentar. Por outro lado, a memória necessária para o processo recursivo cresce
com o número de passos (e pode ser muito pior do que um crescimento linear,
como você verá em breve). É por isso que você deve evitar processos recursivos,
exceto quando tiver certeza de que o número de etapas permanecerá baixo em to-
dos os casos . Como consequência, você deve aprender a substituir a recursão
pela corecursão toda vezé possível.

4.2Eliminação de chamada de cauda

Noneste ponto, você pode ter dúvidas. Eu disse que corecursion usa espaço de pi-
lha constante. Mas você deve saber que uma função que chama a si mesma even-
tualmente usa espaço de pilha, mesmo que não tenha muito para colocar na pilha.
Parece que a corecursion também esgotaria a pilha, embora mais lentamente. É
possível eliminar completamente o problema. O truque é transformar uma fun-
ção correcursiva em um bom e velho loop. Isso não poderia ser mais fácil - basta
substituir a implementação corecursiva

fun toString(list: List<Char>): String {


fun toString(list: List<Char>, s: String): String =
if (lista.isEmpty())
s
senão
toString(list.subList(1, list.size), append(s, list[0]))
return toString(lista, "")
}

com um loop imperativo e referências mutáveis:

fun toStringCorec2(list: List<Char>): String {


var s = ""
for (c na lista) s = append(s, c)
retorno s
}

Mas não se preocupe: Kotlin pode traduzir automaticamente corecursion em lo-


ops para você!
4.2.1 Usando a Eliminação de Chamada Final

Ao contrário do Java, o Kotlin implementa a Eliminação de chamadas de cauda


(TCE). Isso significa que quando uma chamada de função para si mesma é a úl-
tima coisa que a função faz (o que significa que o resultado dessa chamada não é
usado em uma computação posterior), o Kotlin elimina essa chamada final. Mas
não fará isso sem que você peça, o que você pode fazer prefixando a declaração
da função com o tailrec palavra-chave:

fun toString(list: List<Char>): String {


tailrec fun toString(list: List<Char>, s: String): String =
if (lista.isEmpty())
s
senão
toString(list.subList(1, list.size), append(s, list[0]))
return toString(lista, "")
}

Kotlin detecta que esta função é recursiva e aplica o TCE. Novamente, você deve
indicar que esta é sua intenção. Pode parecer chato no começo, e você pode prefe-
rir que o Kotlin lide com isso silenciosamente. Como você descobrirá em breve,
pensar que a implementação de uma função é recursiva quando não é é um bug
comum. Mas se você indicar que sua função tem uma implementação recursiva
de cauda, ​o Kotlin pode verificar a função e informar se você cometeu um erro.
Caso contrário, o Kotlin usaria uma função recursiva sem cauda e um StackO-
verflowException ocorreria potencialmente em tempo de execução. E pior, isso
pode aparecer apenas uma vez na produção porque depende dos dados de
entrada.
4.2.2Mudando de loops para corecursion

Usandocorecursion em vez de loops é uma mudança de paradigma. A princípio,


você pensará em seu paradigma original e depois o traduzirá para o novo. Só
mais tarde você começará a pensar diretamente em seu novo paradigma. Isso é
verdade para todo aprendizado, e aprender a usar corecursion em vez de loops
não é diferente.

Nas seções anteriores, apresentei recursão e correcursão como conceitos nativos.


Neste ponto, uma justificativa para traduzir loops imperativos em funções recur-
sivas provavelmente será útil. (Mas lembre-se de que esta é apenas uma etapa in-
termediária. Em breve você aprenderá como abstrair a recursão e a correcursão
para evitar sua manipulação.)

Embora você tenha visto que a recursão é muito menos útil que a corecursão, de-
vido às limitações de memória implícitas, a recursão tem uma grande vantagem:
geralmente é muito mais simples de escrever. A versão recursiva da soma dos in-
teiros de 1 a 10 pode ser escrita como no exemplo a seguir, assumindo que
sum(n) significa soma dos inteiros de 1 a n :

fun sum(n: Int): Int = if (n < 1) 0 else n + sum(n - 1)

Não poderia ser mais simples. Mas como você viu, a adição não pode ser realizada
até que você saiba o resultado de sum(n - 1) . O estado da computação neste es-
tágio deve ser armazenado na pilha até que todas as etapas tenham sido processa-
das. Você desejará transformar isso em uma implementação corecursiva para se
beneficiar do TCE. Muitos programadores que estão aprendendo a usar corecur-
sion têm problemas com essa transformação, embora seja simples.
Vamos fazer uma pausa e ver como os programadores tradicionais resolveriam o
problema. Os programadores tradicionais desenhariam um fluxograma da com-
putação usando mutação de estado e teste de condição. O problema é tão simples
que eles provavelmente desenhariam o fluxograma apenas em suas cabeças, mas
vamos ver o fluxograma ( figura 4.5 ).

Figura 4.5 Um fluxograma do cálculo imperativo da soma dos n primeiros inteiros


A implementação imperativa correspondente é onipresente, usando o que os pro-
gramadores imperativos chamam de estruturas de controle , como for ou whi-
le laços. Como Kotlin não tem for loop (pelo menos não o loop tradicional
for que corresponde a este fluxograma), vou usar um while loop neste código:

soma divertida(n: Int): Int {


var soma = 0
var idx = 0
while(idx <= n) {
soma += idx
idx += 1
}
soma de retorno
}

Não é grande coisa, mas esse código contém muitos lugares onde pode dar errado,
especialmente porque tive que traduzir o fluxograma em uma while implemen-
tação de loop. É fácil mexer com as condições. Deve ser <= ou < ? Deve i ser in-
crementado antes ou depois s ? Obviamente, esse estilo de programação é para
programadores inteligentes que não cometem esse tipo de erro. Mas para o resto
de nós, que precisaria escrever muitos testes para verificar possíveis erros, é pos-
sível escrever uma implementação corecursiva fazendo a mesma coisa? Claro.

O que você pode fazer é substituir as variáveis ​por parâmetros adicionados à fun-
ção. Em vez de uma função sum(n) , você deve escrever uma função auxiliar:
`sum(n, sum, idx) :

fun soma(n: Int, soma: Int, idx: Int): Int = ...


Em seguida, sua função principal sum(n: Int) chamará a função auxiliar com
os valores iniciais:

soma divertida(n: Int, soma: Int, idx: Int): Int =


if (idx < 1) sum else sum(n, sum + idx, idx - 1)

fun soma(n: Int) = soma(n, 0, n)

Mas, na verdade, n nunca vai mudar. Você pode se beneficiar das funções locais
do Kotlin para remover um parâmetro, tornando a função auxiliar local para a
função principal e fechando sobre o parâmetro da função principal. Isso é muito
mais fácil de fazer do que descrever:

soma divertida(n: Int): Int {


fun sum(soma: Int, idx: Int): Int =
if (idx < 1) sum else -sum(soma + idx, idx - 1)
retorna soma(0, n)
}

Como eu disse anteriormente, agora você precisa de uma função de bloco com um
explícito return no final do bloco e um tipo de retorno explícito. Então, você pre-
cisa implementar a função auxiliar.

Pense na versão em loop. Em cada iteração do loop, você obtém versões modifica-
das das variáveis: s + i e i + 1 . Tudo o que você precisa fazer é fazer com que
a função auxiliar chame a si mesma com estes parâmetros modificados:
soma divertida(n: Int): Int {
fun soma(s: Int, i: Int): Int = soma(s + i, i + 1)
retorna soma(0, 0)
}

Isso não funcionará porque nunca termina. O que você está perdendo é o teste de
condição terminal:

soma divertida(n: Int): Int {


fun sum(s: Int, i: Int): Int = if (i > n) s else sum(s + i, i + 1)
retorna soma(0, 0)
}

Tudo o que resta a fazer é informar ao Kotlin que você deseja que o TCE seja apli-
cado à função auxiliar. Você faz isso adicionando o tailrec palavra-chave:

soma divertida(n: Int): Int {


tailrec fun sum(s: Int, i: Int): Int =
if (i > n) s senão soma(s + i, i + 1)
retorna soma(0, 0)
}

Caso o TCE não possa ser aplicado, o Kotlin exibe um aviso:

Aviso:(16, 5) Kotlin: uma função é marcada como recursiva de cauda


mas nenhuma chamada final foi encontrada
Isso não impedirá a compilação do seu programa, mas produzirá um código que
pode transbordar a pilha! Você deve observar esses avisos de perto.

Exercício 4.1

Implemente uma função correcursiva add trabalhando com inteiros positivos. A


implementação da add funçãonão deve usar os operadores de mais (+) ou menos
(-), mas apenas duas funções:

fun inc(n: Int) = n + 1


fun dez(n: Int) = n - 1

Aqui está a assinatura da função:

fun add(a: Int, b: Int): Int

Dica

Você deve ser capaz de escrever diretamente a implementação corecursiva. Caso


contrário, escreva uma implementação usando um loop e traduza como você
fezpara a sum função.

Solução

Para adicionar dois números, x e y , você pode fazer o seguinte:

Se y = 0, retorna x .
Caso contrário, some 1 a x , subtraia 1 de y e comece de novo.
Isso pode ser escrito usando um loop da seguinte maneira:

fun add(a: Int, b: Int): Int {


var x = a
var y = b
while(y != 0) {
x = inc(x)
y = dez(y)
}
retornar x
}

Aqui, a condição foi alterada para melhor ajustar um while loop. Seria possível
usar a condição original mas com um resultado feio:

fun add(a: Int, b: Int): Int {


var x = a
var y = b
while(verdadeiro) {
se (y == 0) retorna x
x = inc(x)
y = dez(y)
}
}

Observe também que, ao contrário do Java, o Kotlin não permite o uso dos parâ-
metros x e y diretamente porque os parâmetros podem ser apenas val referên-
cias. Você terá que fazer cópias. Então tudo que você precisa fazer é substituir as
variáveis ​por parâmetros em uma chamada para a add função:
tailrec fun add(x: Int, y: Int): Int = if (y == 0) x else add(inc(x), dec(y))

Neste exercício, você não precisa modificar nada. Em vez de armazenar os valo-
res atuais em var referências mutáveis, você precisa chamar recursivamente a
função com os novos valores como parâmetros. Agora você pode chamar sua fun-
ção com qualquer valor de argumento sem causar um StackOverflowExcep-
tion . Como você verá em breve, escrever programas mais seguros geralmente
envolve muito esforço para transformar uma implementação de função recursiva
(não caudal) em uma recursiva caudal. E às vezes, não serápossível!

4.2.3 Usando funções de valor recursivas

Comovocê acabou de ver, definir uma função recursiva é simples. Às vezes, a im-
plementação mais simples é a cauda recursiva, como você viu no exemplo ante-
rior. Mas este não foi um exemplo real. Ninguém jamais criaria esse tipo de fun-
ção para realizar uma adição. Vamos tentar com um exemplo mais útil. A função
factorial(int n) podeser definido como retornando 1 se seu argumento for
0, e n * factorial(n – 1) caso contrário:

fun fatorial(n: Int): Int = if (n == 0) 1 else n * fatorial(n - 1)

Esta função, obviamente, não é recursiva, portanto, esteja ciente de que ela trans-
bordará a pilha se n for maior que alguns milhares. E não use esse tipo de código
em produção, a menos que você tenha certeza de que o número de etapas de re-
cursão permanecerá baixo.

Escrever fun funções recursivas é fácil. E as funções de valor recursivas?


Exercício 4.2 (difícil)

Escreva uma função de valor fatorial recursiva. Lembre-se que uma função de va-
lor é uma função declarada com a val palavra-chave:

val fatorial: (Int) -> Int =

Como este é um exercício, usar uma referência de função seria considerado


trapaça!

Dica

Você pode consultar o capítulo 2, seções “Inicialização lenta” e “Inicialização atra-


sada”, para resolver este exercício.

Solução

Como a função deve chamar a si mesma, ela já deve estar definida quando essa
chamada acontecer, o que implica que ela deve ser definida antes de você tentar
defini-la! Deixe de lado esse problema do ovo e da galinha por enquanto. Conver-
tendo um único argumento fun função em uma função de valor é simples. Ele
usa um lambda com a mesma implementação da fun função:

val fatorial: (Int) -> Int =


{ n -> if (n <= 1) n else n * fatorial(n - 1) }
OBSERVAÇÃO Você precisa indicar explicitamente o tipo da função ou o tipo do
argumento lambda.

Agora para a parte mais complicada. Este código não compilará porque o compila-
dor reclamará que a variável factorial ainda não foi inicializada. O que isto
significa? Quando o compilador lê este código, está no processo de definição
do factorial função. Durante esse processo, ele encontra uma chamada para a
função fatorial, que ainda não está definida. Este é o mesmo problema que

val x: Int = x + 1

Você pode resolver esse problema declarando primeiro a variável e depois alte-
rando seu valor, o que pode ser feito em um inicializador como o seguinte:

lateinit var x: Int


iniciar {
x = x + 1;
}

Isso funciona porque os membros são definidos antes da execução dos inicializa-
dores. o lateinit A palavra-chave declara que a variável será inicializada poste-
riormente. Se você chamá-lo antes de ser inicializado, receberá uma exceção. Mas
esta função permite usar um tipo não anulável. Sem lateinit , você seria for-
çado a usar um tipo anulável ou inicializar a referência a um valor fictício. Este
truque é totalmente inútil no caso anterior, mas você pode usá-lo para definir a
função fatorial:
objeto fatorial {
lateinit var fatorial: (Int) -> Int
iniciar {
fatorial = { n -> if (n <= 1) n else n * fatorial(n - 1) }
}
}

Outra solução mais elegante é usar a inicialização preguiçosa:

objeto fatorial {
val fatorial: (Int)-> Int por preguiçoso { { n: Int ->
if (n <= 1) n else n * fatorial(n - 1)
} }
}

As chaves duplas sãoobrigatoriedade! A inicialização preguiçosa é obtida com o


seguinte:

objeto fatorial {
val fatorial: (Int)-> Int por lazy { ... }
}

O ... agora deve ser substituído pelo lambda, que é quase o mesmo do exemplo
anterior, incluindo as chaves:

{ n: Int -> if (n <= 1) n else n * fatorial(n - 1) }


A única diferença é que o Kotlin agora não consegue inferir o tipo de n , então
você deve escrevê-lo explicitamente. O único problema com esse truque é que o
campo não pode ser declarado como val , o que é irritante porque a imutabili-
dade é uma das técnicas fundamentais para uma programação segura. Com um
var , nada garante que o valor da factorial variável não mude depois. Uma so-
lução é tornar o var privado e, em seguida, copiar seu valor para um val :

objeto fatorial {
private lateinit var fato: (Int) -> Int
iniciar {
fato = { n -> if (n <= 1) n else n * fato(n - 1) }
}
val fatorial = fato
}

Dessa forma, você pode ter certeza de que nada pode alterar o valor de factori-
al uma vez inicializado. Mas lembre-se, uma função de valor recursiva, embora
possa ser recursiva de cauda, ​não pode ser otimizada por meio do TCE, portanto,
pode estourar a pilha. Se você precisar de uma função de valor recursiva de
cauda, ​use uma referência de função.

Observe também que esta função não funcionará em nenhum caso para valores
acima de 16, pois causará um estouro aritmético, produzindo um resultado nega-
tivo. Pior, acima de 33, produzirá um resultado 0 porque a multiplicação de –
2.147.483.648 (o resultado da chamada da função com o parâmetro 33) por 34 re-
sulta em 0. Isso faz com que todos os resultados subsequentes sejam0.(Isso acon-
tece porque Int os valores Kotlin são números de 32 bits.)
4.3 Funções e listas recursivas

Recursãoe a recursão de cauda são freqüentemente usadas para processar listas.


Esses processos geralmente envolvem a divisão de uma lista em duas partes: o
primeiro elemento, chamado head , e o restante da lista, chamado tail . Você já viu
um exemplo disso ao definir uma função que converte uma lista de caracteres em
uma string. Considere a seguinte função, que calcula a soma dos elementos de
uma lista de inteiros:

fun sum(lista: List<Int>): Int =


if (list.isEmpty()) 0 else list[0] + sum(list.drop(1))

Se a lista estiver vazia, a função retorna 0. Caso contrário, ela retorna o valor do
primeiro elemento (o início da lista) mais o resultado da aplicação da sum função
ao restante da lista (o fim da lista). Pode ser mais claro se você definir funções au-
xiliares para retornar o início e o final de uma lista. Você não precisa restringir
essas funções a listas de números inteiros porque elas podem funcionar em qual-
quer lista:

divertido <T> head(lista: Lista<T>): T =


if (lista.isEmpty())
throw IllegalArgumentException("head chamado na lista vazia")
senão
lista[0]

fun <T> tail(lista: Lista<T>): Lista<T> =


if (lista.isEmpty())
throw IllegalArgumentException("cauda chamada na lista vazia")
senão
lista.drop(1)

fun sum(lista: List<Int>): Int =


if (lista.isEmpty())
0
senão
cabeça(lista) + soma(cauda(lista))

Ou, melhor ainda, você pode criar head e tail funcionar como funções de ex-
tensão do List classe:

fun <T> List<T>.head(): T =


if (this.isEmpty())
throw IllegalArgumentException("head chamado na lista vazia")
senão
isso[0]

divertido <T> Lista<T>.tail(): Lista<T> =


if (this.isEmpty())
throw IllegalArgumentException("cauda chamada na lista vazia")
senão
this.drop(1)

fun sum(lista: List<Int>): Int =


if (lista.isEmpty())
0
senão
lista.cabeça() + soma(lista.cauda())
Neste exemplo, a chamada recursiva para a sum função não é a última coisa que
a função faz. As quatro últimas coisas que a função faz são as seguintes:

Chama a head função


Chama a tail função
Chama a sum funçãocom o resultado de tail como seu argumento
Adiciona o resultado de head e o resultado de sum

Esta função não é recursiva, então você não pode usar o tailrec palavra-chave,
e você não poderá usar esta função com listas de mais de alguns milhares de ele-
mentos. Mas você pode reescrever esta função para colocar a chamada sum na
posição final:

fun sum(lista: List<Int>): Int {


tailrec fun sumTail(list: List<Int>, acc: Int): Int =
if (lista.isEmpty())
acc
senão
sumTail(list.tail(), acc + list.head())
return sumTail(lista, 0)
}

Aqui o sumTail A função auxiliar é recursiva e pode ser otimizada por meio do
TCE. Como essa função auxiliar nunca será usada em outro lugar, o melhor lugar
para colocá-la é dentro da sum função.

Você pode definir a função auxiliar ao lado da função principal. Mas você teria
que tornar pública a função auxiliar private ou internal e a função principal.
Nesse caso, a chamada para a função auxiliar pela função principal seria um fe-
chamento. Os principais motivos para preferir uma função auxiliar definida local-
mente em vez de uma função auxiliar privada é evitar conflitos de nomes e poder
fechar alguns parâmetros da função envolvente.

Em linguagens que permitem funções definidas localmente, uma prática atual é


chamar todas as funções auxiliares com um único nome, como go ou process .
Isso nem sempre pode ser feito com funções não locais; os nomes podem entrar
em conflito se os tipos de argumento forem idênticos. No exemplo anterior, a fun-
ção auxiliar para sum foi chamada sumTail .

Outra prática atual é chamar a função auxiliar com o mesmo nome da função
principal com um sublinhado anexado, como sum_ . Também é possível dar à
função auxiliar o mesmo nome da função principal porque as funções terão uma
assinatura diferente. Seja qual for o sistema escolhido, é útil ser consistente. No
restante deste livro, usarei o sublinhado para denotar funções auxiliares recursi-
vas de cauda.

4.3.1 Usando funções duplamente recursivas

Nãolivro falando sobre funções recursivas pode evitar o exemplo da série Fibo-
nacci. Embora seja totalmente inútil para a maioria de nós, é onipresente porque
é um dos exemplos mais simples de uma função duplamente recursiva , ou seja,
uma função que chama a si mesma duas vezes em cada etapa. Vamos começar
com os requisitos caso você nunca tenha cumprido essa função.

A série de Fibonacci é um conjunto de números em que cada número é a soma


dos dois anteriores. Esta é uma definição recursiva. Você precisa de uma condição
terminal, então os requisitos completos são os seguintes:
f (0) = 1
f (1) = 1
f (n) = f (n – 1) + f (n – 2)

Esta é a série original de Fibonacci, na qual os dois primeiros números são iguais
a 1. Supõe-se que cada número seja uma função de sua posição na série. A maio-
ria dos programadores geralmente prefere começar em 0 em vez de 1. Muitas ve-
zes você encontrará definições onde f(0) = 0, que não faz parte da série original de
Fibonacci. De qualquer forma, isso não muda o problema.

Por que essa função é tão interessante? Em vez de responder a essa pergunta
agora, vamos tentar uma implementação ingênua:

fun fibonacci(número: Int): Int =


if (número == 0 || número == 1)
1
senão
fibonacci(número - 1) + fibonacci(número - 2)

Agora você escreverá um programa simples para testar esta função:

fun main(args: Array<String>) {


(0 até 10).forEach { print("${fibonacci(it)} ") }
}

Se você executar este programa de teste, obterá os dez primeiros números de


Fibonacci:
1 1 2 3 5 8 13 21 34 55

Com base no que você sabe sobre recursão em Kotlin, você pode pensar que essa
função será bem-sucedida no cálculo f(n) de n , até alguns milhares antes de es-
tourar a pilha. Bem, vamos verificar. Substitua 10 por 1000 e veja o que acon-
tece. Inicie o programa e faça uma pausa para o café. Ao retornar, você perceberá
que o programa ainda está em execução. Terá alcançado algo em torno de
1.836.311.903, que é apenas a 47ª etapa (sua milhagem pode variar — você pode
até obter um número negativo!), mas nunca terminará. Sem estouro de pilha, sem
exceção - apenas pendurado na natureza. O que está acontecendo?

O problema é que cada chamada para a função cria duas chamadas recursivas.
Para calcular f(n) , você precisa de 2 n chamadas recursivas. Digamos que sua
função precise de 10 nanossegundos para ser executada. (Apenas supondo, mas
logo você verá que isso não muda nada.) O cálculo f(5000) levará 2 5000 × 10 na-
nossegundos. Você tem ideia de quanto tempo isso é? O programa nunca termi-
nará porque precisaria de mais tempo do que a duração esperada do sistema so-
lar (se não do universo).

Para tornar um utilizávelFunção de Fibonacci, você deve alterá-la para que use
uma única chamada recursiva de cauda. Há também outro problema: os resulta-
dos são tão grandes que logo você terá um estouro aritmético, primeiro resul-
tando em números negativos e logo em 0.

Exercício 4.3

Crie uma versão recursiva de cauda da função Fibonacci.


Dica

Se você pensar em uma implementação baseada em loop, como quando criou a


sum função, você sabe que deve usar duas variáveis ​para acompanhar os dois va-
lores anteriores. Essas variáveis ​seriam então convertidas em parâmetros para
uma função auxiliar. Esses parâmetros serão do tipo BigInteger para permitir o
cálculo de grandes valores.

Solução

Vamos primeiro escrever a assinatura da função auxiliar. Levará duas BigInte-


ger instâncias como parâmetros e uma para o argumento original e retornará um
BigInteger :

tailrec fun fib(val1: BigInteger, val2: BigInteger, x: BigInteger): BigInteger

Você deve lidar com as condições terminais. Se o argumento for 0 , você retorna
1:

tailrec
fun fib(val1: BigInteger, val2: BigInteger, x: BigInteger): BigInteger =
quando {
(x == BigInteger.ZERO) -> BigInteger.ONE
...
}

Se o argumento for 1 , você retorna a soma dos dois parâmetros val1 e val2 :
tailrec
fun fib(val1: BigInteger, val2: BigInteger, x: BigInteger): BigInteger =
quando {
(x == BigInteger.ZERO) -> BigInteger.ONE
(x == BigInteger.ONE) -> val1 + val2
...
}

Eventualmente, você tem que lidar com a recursão. Para fazer isso, você deve fa-
zer o seguinte:

Pegue val2 e faça val1 .


Crie um novo val2 adicionando os dois valores anteriores.
Subtraia 1 do argumento.
Chame recursivamente a função com os três valores calculados como seus
argumentos.

Aqui está a transcrição em código:

tailrec
fun fib(val1: BigInteger, val2: BigInteger, x: BigInteger): BigInteger =
quando {
(x == BigInteger.ZERO) -> BigInteger.ONE
(x == BigInteger.ONE) -> val1 + val2
else -> fib(val2, val1 + val2, x - BigInteger.ONE)
}

Os dois parâmetros val1 e val2 acumular os resultados fib(n - 1) e fib(n


- 2) . Por esse motivo, esses são frequentemente chamados acc (para acumula-
dor ). Aqui você pode renomeá-los acc1 e acc2 . A última coisa a fazer é criar a
função main que chama essa função helper com os parâmetros iniciais, e colocar
a função helper dentro do corpo da função main:

fun fib(x: Int): BigInteger {


tailrec
fun fib(val1: BigInteger, val2: BigInteger, x: BigInteger): BigInteger =
quando {
(x == BigInteger.ZERO) -> BigInteger.ONE
(x == BigInteger.ONE) -> val1 + val2
else -> fib(val2, val1 + val2, x - BigInteger.ONE)
}
return fib(BigInteger.ZERO, BigInteger.ONE, BigInteger.valueOf(x.toLong()))
}

Esta é apenas uma implementação possível. Você pode organizar parâmetros, va-
lores iniciais e condições de uma maneira ligeiramente diferente, desde que fun-
cione. Agora você pode ligar fib(10_000) e ele lhe dará o resultado em alguns
nanossegundos. Bem, levará algumas dezenas de milissegundos, mas apenas por-
que a impressão no console é uma operação lenta. Voltarei a isso em breve. De
qualquer forma, o resultado é impressionante, seja pelo resultado da computação
(2.090 dígitos) ou pelo aumento de velocidade devido à transformação de uma
chamada dual recursiva em uma única corecursiva1.

4.3.2 Abstraindo a recursão em listas

UmO principal uso da recursão consiste em combinar o primeiro elemento (a ca-


beça) de uma lista com o resultado da aplicação do mesmo processo ao restante
da lista (a cauda). Você já viu um exemplo disso quando calculou a soma de uma
lista de inteiros, que você definiu como

fun sum(lista: List<Int>): Int =


if (lista.isEmpty())
0
senão
lista.cabeça() + soma(lista.cauda())

O mesmo princípio pode ser aplicado a qualquer operação em quaisquer tipos e


não apenas à adição de inteiros. Você já viu um exemplo que consiste em proces-
sar uma lista de caracteres para construir uma string a partir deles. Você pode
usar a mesma técnica para copiar uma lista de qualquer tipo em uma string
delimitada:

fun <T> makeString(lista: Lista<T>, delim: String): String =


quando {
list.isEmpty() -> ""
list.tail().isEmpty() ->
"${list.head()}${makeString(list.tail(), delim)}"
else -> "${list.head()}$delim${makeString(list.tail(), delim)}"
}

Exercício 4.4

Escreva uma versão recursiva de cauda do makeString função. (Tente não olhar
para a versão recursiva de cauda de sum .)
Solução

Você precisa aplicar a mesma técnica que usou no exemplo anterior: criar uma
função auxiliar com um parâmetro adicional acumulando o resultado. Se você co-
locar essa função auxiliar dentro da função principal, poderá fazê-la fechar sobre
a delim parâmetro porque não muda a cada passo recursivo:

fun <T> makeString(lista: Lista<T>, delim: String): String {


tailrec fun makeString_(list: List<T>, acc: String): String = quando {
list.isEmpty() -> acc
acc.isEmpty() -> makeString_(list.tail(), "${list.head()}")
else -> makeString_(list.tail(), "$acc$delim${list.head()}")
}
return makeString_(lista, "")
}

Ok, era simples, mas repetir isso para cada função recursiva seria tedioso. Você
pode abstrair isso? Você pode. A primeira coisa a fazer é dar um passo para trás e
dar uma olhada na figura inteira. O que você tem?

Uma função trabalhando em uma lista de elementos de um determinado tipo,


retornando um único valor de outro tipo. Esses tipos podem ser abstraídos em
parâmetros de tipo T e arquivos U .
Uma operação entre um elemento de tipo T e um elemento de tipo U , produ-
zindo um resultado de tipo U . Observe que tal operação é uma função de um
par (U, T) para um U .

Isso parece ser diferente do que você tinha no sum exemplo, mas na verdade é o
mesmo, embora T e U fossem do mesmo tipo ( Int ). Para o string exemplo,
T was Char e U was String . Para o makeString function, T já era genérico e
U era String .

Exercício 4.5

Crie uma versão genérica de sua função recursiva de cauda que pode ser usada
para sum , string e makeString . Chame esta função foldLeft , então escreva
sum , string , e makeString em termos desta nova função.

Dica

Você terá que introduzir o valor inicial do tipo U (para o acumulador) e a função
de (U, T) a U como parâmetros adicionais.

Solução

Aqui está a foldLeft implementação da função e as três funções modificadas


para usar a foldLeft abstração:

fun <T, U> foldLeft(list: List<T>, z: U, f: (U, T) -> U): U {


tailrec fun foldLeft(list: List<T>, acc: U): U =
if (lista.isEmpty())
acc
senão
foldLeft(list.tail(), f(acc, list.head()))
return foldLeft(lista, z)
}

fun sum(lista: List<Int>) = foldLeft(lista, 0, Int::plus)


fun string(list: List<Char>) = foldLeft(list, "", String::plus)

fun <T> makeString(lista: Lista<T>, delim: String) =


foldLeft(list, "") { s, t -> if (s.isEmpty()) "$t" else "$s$delim$t" }

A função que você criou aqui é uma das mais importantes na hora de programar
sem loops. Essa função permite que você abstraia a corecursão de maneira segura
para a pilha, de modo que raramente terá que pensar em tornar as funções recur-
sivas. Mas às vezes você precisará fazer as coisas da maneira oposta, usando re-
cursão em vez de corecursão.

Suponha que você tenha uma lista de caracteres [a, b, c] e queira construir a
string "abc" usando apenas o head e tail e prepend funções que você desen-
volveu no anteriorseção. Você não pode acessar os elementos da lista por seu ín-
dice. Mas você poderia escrever a seguinte implementação recursiva:

fun string(lista: List<Char>): String =


if (lista.isEmpty())
""
senão
prepend(list.head(), string(list.tail()))

Você pode abstrair várias coisas neste código da mesma forma que abstraiu a
foldLeft função. Você pode abstrair o Char tipo a ser digitado T para que a
função funcione com listas de qualquer tipo. Você pode abstrair o String tipo de
retorno para U poder construir um resultado de qualquer tipo. E você teria que
abstrair a prepend função em uma função genérica (T, U) -> U . Você tam-
bém deve substituir o valor inicial "" pelo identity valor do tipo U correspon-
dente a esta função.

Exercício 4.6

Escreva esta função abstrata e chame-a foldRight . Em seguida, escreva a


string função em termos de foldRight .

Solução

Escreva a assinatura da função, tomando como parâmetros adicionais a identi-


dade e a função a ser usada para dobrar:

fun <T, U> foldRight(list: List<T>, identity: U, f: (T, U) -> U): U =

Se a lista estiver vazia, retorne o identity valor:

fun <T, U> foldRight(list: List<T>, identity: U, f: (T, U) -> U): U =


if (lista.isEmpty())
identidade

Se a lista não estiver vazia, use o mesmo código da string função, substituindo
prepend pela função de parâmetro:

fun <T, U> foldRight(list: List<T>, identity: U, f: (T, U) -> U): U =


if (lista.isEmpty())
identidade
senão
f(list.head(), foldRight(list.tail(), identidade, f))

É isso! Agora você pode definir string em termos foldRight chamando-o com
os valores que você substituiu pelos tipos genéricos:

fun string(lista: List<Char>): String =


foldRight(list, "", { c, s -> preceder(c, s) })

É mais idiomático em Kotlin escrever o último argumento fora dos parênteses


quando esse argumento é uma função. Nesse caso, nenhuma vírgula é usada an-
tes da função:

fun string(lista: List<Char>): String =


foldRight(list, "") { c, s -> preceder(c, s) }

OBSERVAÇÃO A foldRight função é recursiva; não é recursivo de cauda, ​por-


tanto não pode ser otimizado usando o TCE. Você não pode criar uma versão re-
cursiva de cauda real de foldRight . A única possibilidade é definir uma função
retornando o mesmo resultado de foldRight , mas usando uma dobra à es-
querda, por exemplo, após inverter a lista.

Ao usar a List classe do Kotlin, você não precisa criar foldRight e fol-
dLeft porque o Kotlin já define essas funções (embora foldLeft seja chamado
simplesmente fold ).
4.3.3 Invertendo uma lista

Invertendouma lista às vezes é útil, embora essa operação geralmente não seja
ideal em termos de desempenho. Encontrar outras soluções que não exijam a in-
versão de uma lista é preferível, mas nem sempre é possível. Uma dessas soluções
é usar uma estrutura de dados diferente que permita o acesso de ambas as
extremidades.

Definir uma reverse função com uma implementação baseada em loop é fácil:
iterar para trás na lista. Você deve ter cuidado, porém, para não mexer com os
índices:

fun <T> reverse(lista: Lista<T>): Lista<T> {


resultado val: MutableList<T> = mutableListOf()
(list.size downTo 1).forEach {
result.add(list[it - 1])
}
resultado de retorno
}

Mas não é assim que deve ser feito em Kotlin. Kotlin já tem um reversed função
em listas.

Exercício 4.7

Defina uma reverse função usando uma dobra.


Dica

Lembre-se que foldRight pode transbordar a pilha quando usado com listas
longas, então você deve preferir foldLeft sempre que possível. Você também
deve criar a prepend função trabalhando na lista e adicionando um elemento na
frente da lista. Não se preocupe com o desempenho. Esse é um problema que você
abordará no capítulo 5. Faça sua função funcionar com listas imutáveis ​usando o
+ operador.

Solução

A prepend função é fácil de definir, embora haja um pequeno truque.O + opera-


dor em Kotlin permite concatenar listas ou adicionar um elemento no final de
uma lista, mas não adicionar um elemento no início da lista. Uma solução para
este problema é primeiro fazer uma única lista de itens do elemento para
preceder:

fun <T> prepend(list: List<T>, elem: T): List<T> = listOf(elem) + list

Então, tudo que você precisa fazer para inverter uma lista é dobrá-la para a es-
querda usando esta função:

fun <T> reverse(lista: Lista<T>): Lista<T> =


foldLeft(list, listOf(), ::prepend)

Isso funciona, mas é uma espécie de trapaça. Você poderia definir o reverse fun-
ção sem criar esta lista de itens únicos?
Exercício 4.8

Defina a reverse função usando apenas a versão anexada + sem recorrer à


concatenação.

Dica

O que você precisa para este exercício é definir a prepend função sem usar con-
catenação. Tente começar com uma função copiando uma lista através de uma
dobra esquerda.

Solução

Copiar uma lista através de uma dobra esquerda é fácil:

fun <T> copy(lista: Lista<T>): Lista<T> =


foldLeft(list, listOf()) { lst, elem -> lst + elem }

A prepend função que adiciona um elemento na frente de uma lista pode ser im-
plementada dobrando a lista à esquerda, usando um acumulador contendo o ele-
mento a adicionar em vez da lista vazia:

fun <T> preceder(lista: Lista<T>, elem: T): Lista<T> =


foldLeft(list, listOf(elem)) { lst, elm -> lst + elm }

Agora você pode usar a mesma reverse implementação com esta nova pre-
pend função:
fun <T> reverse(lista: Lista<T>): Lista<T> =
foldLeft(list, listOf(), ::prepend)

Não use essas implementações de reverse e prepend no código de produção.


Ambos implicam percorrer toda a lista várias vezes, portanto, são lentos. Se você
estiver trabalhando com listas Kotlin, use a reversed função padrão em List .
No capítulo 5, você aprenderá como criar listas funcionais imutáveis ​que funcio-
nam bem em todos osocasiões.

4.3.4 Construindo listas correcursivas

Umcoisa que os programadores fazem repetidamente é construir listas corecursi-


vas, e a maioria delas são listas de números inteiros. Considere o seguinte exem-
plo em Java:

for (int i = 0; i < limite; i++) {


// algum processamento...
}

Este código é uma composição de duas abstrações: uma lista corecursiva e algum
processamento. A lista corecursiva é uma lista de inteiros de 0 (incluídos) a
limit (excluídos). Como eu disse, uma maneira de tornar os programas mais se-
guros é, entre outras coisas, levar a abstração ao limite para que você possa reuti-
lizar o máximo de código. Vamos abstrair a construção dessa lista correcursiva.

As listas correcursivas são fáceis de construir. Você começa do primeiro elemento


( int i = 0 ) e aplica a função escolhida ( i -> i++ ).
Você poderia ter construído a lista primeiro e depois mapeado para uma função
correspondente a some processing... , ou para uma composição de funções,
ou para um efeito. Vamos fazer isso com um limite concreto. Considere este exem-
plo de Java:

for (int i = 0; i < 5; i++) {


System.out.println(i);
}

Isso é quase equivalente ao seguinte código Kotlin:

listOf(0, 1, 2, 3, 4).forEach(::println)

Tanto a lista quanto o efeito foram abstraídos. Mas você pode levar a abstração
ainda mais longe.

Exercício 4.9

Escreva uma implementação baseada em loop de uma função que produza uma
lista usando um valor inicial, um limite e a função x -> x + 1 . Você vai chamar
issofunction range , e terá a seguinte assinatura:

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

Solução

Você pode usar um while loop para implementar a range função:


fun range(start: Int, end: Int): List<Int> {
resultado val: MutableList<Int> = mutableListOf()
var índice = início
while (índice < fim) {
result.add(index)
index++
}
resultado de retorno
}

Exercício 4.10

Escreva uma versão genérica de uma função semelhante a um intervalo que fun-
cione para qualquer tipo e qualquer condição. Como a noção de intervalo funci-
ona apenas para números, vamos chamar essa função unfold e dê-lhe a seguinte
assinatura:

fun <T> desdobrar(seed: T, f: (T) -> T, p: (T) -> Boolean): List<T>

Solução

A partir da range implementação da função, basta substituir as partes específi-


cas por outras genéricas:

fun <T> desdobrar(seed: T, f: (T) -> T, p: (T) -> Boolean): List<T> {


resultado val: MutableList<T> = mutableListOf()
var elem = semente
enquanto (p(elem)) {
result.add(elem)
elem = f(elem)
}
resultado de retorno
}

Exercício 4.11

Implemente a range função em termos de unfold .

Solução

Não há nada difícil aqui. Você precisa fornecer

O seed , que é o start parâmetro de range


A função f , que é { x -> x + 1 } ou o equivalente { it + 1 }
O predicado p , que resolve { x -> x < end } ou o equivalente { it <
end } :

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


desdobrar(inicio, { it + 1 }, { it < end })

Correcursão e recursão têm um relacionamento duplo. Um é a contraparte do ou-


tro, então sempre é possível transformar um processo recursivo em um corecur-
sivo e vice-versa. Por enquanto, vamos fazer o processo inverso.

Exercício 4.12

Escreva uma versão recursiva de range com base nas funções definidas nas se-
ções anteriores.
Dica

A única função necessária é prepend , embora você possa escolher outras imple-
mentações usando funções diferentes.

Solução

Definir uma implementação recursiva é extremamente simples. você prepen-


d o start parâmetro para a mesma função, usando o mesmo end parâmetro e
substituindo o start parâmetro pelo resultado da aplicação da f função a ele. É
muito mais fácil fazer do que explicar em palavras:

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


if (fim <= início)
lista de()
senão
prepend(range(start + 1, end), start)

Exercício 4.13

Escreva uma versão recursiva de unfold .

Dica

Novamente, comece com a range implementação recursiva da função e tente


gerá-la.
Solução

A solução é simples:

fun <T> desdobrar(seed: T, f: (T) -> T, p: (T) -> Boolean): List<T> =


se (p(semente))
prepend(unfold(f(seed), f, p), seed)
senão
lista de()

Agora você pode redefinir range em termos desta função. Esteja ciente, no en-
tanto, que a unfold função recursivaexplodirá a pilha após alguns milhares de
passos recursivos.

Exercício 4.14

Você pode fazer uma versão recursiva da cauda desta função? Tente responder a
esta pergunta na teoria antes de fazer o exercício.

Dica

Pense sobre isso: é unfold uma função recursiva ou corecursiva?

Solução

A unfold função é de fato correcursiva, como o foldLeft função que você de-
senvolveu anteriormente. Você pode imaginar que pode fazer uma versão recur-
siva usando uma função auxiliar usando um acumulador como um parâmetro
adicional:
fun <T> desdobrar(seed: T, f: (T) -> T, p: (T) -> Boolean): List<T> {
tailrec divertido desdobrar_(acc: List<T>,
semente: T,
f: (T) -> T, p: (T) -> Booleano): List<T> =
se (p(semente))
desdobrar_(acc + semente, f(semente), f, p)
senão
acc
return desdobra_(listaOf(), semente, f, p)
}

O uso de uma função local permite simplificar este código removendo os parâme-
tros constantes da função auxiliar ( f e p ) e fazendo esta função fechar sobre a
função envolventeParâmetros:

fun <T> desdobrar(seed: T, f: (T) -> T, p: (T) -> Boolean): List<T> {


tailrec fun develop_(acc: List<T>, seed: T): List<T> =
se (p(semente))
desdobrar_(acc + semente, f(semente))
senão
acc
return desdobra_(listaOf(), semente)
}

4.3.5 O perigo do rigor

Nenhumdessas versões (recursiva e corecursiva) são equivalentes ao for loop.


Isso ocorre porque mesmo com linguagens rígidas como Java e Kotlin (elas são rí-
gidas em relação aos argumentos de método ou função), o for loop, como a maio-
ria das estruturas de controle, é preguiçoso. Isso significa que no for loop que
você usou como exemplo, a ordem de avaliação será índice, computação, índice,
computação ..., embora usar a range função primeiro calcule a lista completa de
índices antes de mapear a função.

Esse problema surge porque você não deveria usar listas para isso. As listas são
estruturas de dados estritas. Mas você tem que começar de algum lugar. No capí-
tulo 9, você aprenderá a usar coleções preguiçosas que resolverãoistoproblema.

4.4Memorização

DentroNa seção 4.3.1, você implementou uma função para exibir uma série de nú-
meros de Fibonacci. Um problema com essa implementação da série de Fibonacci
é que, se você quiser imprimir a string que representa a série até f(n) , terá que
calcular f(1) , f(2) , até f(n) . Mas para calcular f(n) , você precisa calcular
recursivamente a função para todos os valores anteriores. Eventualmente, para
criar a série até n , você terá calculado f(1) n vezes, f(2) n – 1 vezes e assim
por diante. O número total de computações será então a soma dos inteiros 1 a n .

Nesta seção, você aprenderá como a memorização pode ajudar. Memorização é a


técnica de manter o resultado de uma computação na memória para que possa
ser retornado imediatamente se você tiver que refazer a mesma computação no
futuro. Você pode fazer melhor? Uma possibilidade seria implementar uma fun-
ção especial chamada scan . Você fará isso no capítulo 8. Por enquanto, veremos
outra solução. Você poderia manter os valores calculados na memória para não
precisar computá-los novamente se forem necessários várias vezes?
4.4.1 Usando memoização em programação baseada em loop

Dentroprogramação baseada em loop, você não teria esse problema. A maneira


óbvia de proceder seria a seguinte:

fun main(args: Array<String>) {


println(fibo(10))
}

fun fibo(limite: Int): String =


quando {
limite < 1 -> lance IllegalArgumentException ()
limite == 1 -> "1"
senão -> {
var fibo1 = BigInteger.ONE
var fibo2 = BigInteger.ONE
var fibonacci: BigInteger
construtor val = StringBuilder("1, 1")
for (i em 2 até o limite) {
fibonacci = fibo1.add(fibo2)
builder.append(", ").append(fibonacci) ①
fibo1 = fibo2 ②
fibo2 = fibonacci ③
}
construtor.toString()
}
}

① Acumula o resultado atual no acumulador (o StringBuffer)

② Armazena f(n – 1) para a próxima passagem


③ Armazena f(n) para a próxima passagem

Embora este programa concentre a maioria dos problemas que a programação


funcional deveria evitar ou resolver, ele funciona e é muito mais eficiente que a
versão funcional. O motivo é a memoização.

Como eu disse, memoização é a técnica de manter o resultado de uma computa-


ção na memória para que possa ser retornado imediatamente se você tiver que
refazer a mesma computação no futuro. Aplicada a funções, a memoização faz
com que as funções memorizem oresultados de chamadas anteriores, para que
possam retornar os resultados muito mais rapidamente se forem chamados nova-
mente com os mesmos argumentos.

Isso pode parecer incompatível com os princípios que expus anteriormente por-
que uma função memorizada mantém um estado mutável, que é um efeito colate-
ral. Mas não é incompatível porque o resultado da função é o mesmo quando é
chamada com o mesmo argumento. (Você poderia argumentar que é ainda mais o
mesmo porque não é calculado novamente!) O efeito colateral de armazenar os
resultados não deve ser visível de fora da função. Na programação tradicional,
como a manutenção do estado é a maneira universal de calcular os resultados, a
memoização nem épercebido.

4.4.2 Usando memoização em funções recursivas

recursivoas funções geralmente usam memoização implicitamente. No exemplo


do Fibonacci recursivofunção, você queria retornar a série, então calculou cada
número na série, levando a recálculos desnecessários. Uma solução simples é re-
escrever a função para retornar diretamente a string que representa a série.
Exercício 4.15

Escreva uma função tail-recursive tomando um inteiro n como seu argumento e


retornando uma string representando os valores dos números de Fibonacci de
0 a n , separados por uma vírgula e um espaço.

Dica

Uma solução é usar uma instância de StringBuilder como acumulador.


StringBuilder é uma estrutura mutável, mas essa mutação não será visível de
fora. Outra solução é retornar uma lista de números e transformá-la em um ar-
quivo String . Essa solução é mais fácil porque você pode abstrair o problema
dos separadores retornando primeiro uma lista e depois escrevendo uma função
para transformar a lista em uma string separada por vírgula.

Solução

A listagem a seguir mostra a solução usando List comoacumulador.

Listagem 4.1 Fibonacci recursivo com memoização implícita

fun fibo(número: Int): String {


tailrec fun fibo(acc: List<BigInteger>,
acc1: BigInteger,
acc2: BigInteger, x: BigInteger): List<BigInteger> =
quando (x) {
BigInteger.ZERO -> acc
BigInteger.ONE -> acc + (acc1 + acc2)
else -> fibo(acc + (acc1 + acc2), acc2, acc1 + acc2, ①
x - BigInteger.ONE)
}
lista val = fibo(listaOf(), ②
BigInteger.ONE, BigInteger.ZERO, BigInteger.valueOf(number.toLong())
return makeString(lista, ", ") ③
}

fun <T> makeString(lista: Lista<T>, separador: String): String =


quando {
list.isEmpty() -> ""
list.tail().isEmpty() -> list.head().toString()
else -> list.head().toString() + ④
foldLeft(list.tail(), "") { x, y -> x + separador + y }
}

① O primeiro sinal + nesta linha é o operador de concatenação da lista; outros repre-


sentam a adição de BigIntegers.

② Chama a função auxiliar fibo para obter a lista de números de Fibonacci

③ Formata a lista em uma string separada por vírgulas por meio de uma chamada
para makeString. Isto é apenas para um exercício. Você poderia ter usado a fun-
ção list.joinToString (", ") padrão do Kotlin.

④ O sinal + deve estar no final desta linha e não no início da linha seguinte; caso con-
trário, o código não será compilado.

4.4.3 Usando memoização implícita

esteexemplo demonstra o uso de memoização implícita. Não conclua que esta é a


melhor maneira de resolver o problema. Muitos problemas são muito mais fáceis
de resolver quando distorcidos - vistos de outra perspectiva. Vamos torcer este.

Em vez de um conjunto de números, você pode ver a série de Fibonacci como um


conjunto de pares (tuplas de dois elementos). Em vez de tentar gerar isso

1, 1, 2, 3, 5, 8, 13, 21,...

você poderia tentar produzir isso:

(1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21), ...

Nesta série, cada tupla pode ser construída a partir da anterior. O segundo ele-
mento da tupla n torna-se o primeiro elemento da tupla n + 1. O segundo ele-
mento da tupla n + 1 é igual à soma dos dois elementos da tupla n . Em Kotlin,
você pode escrever uma função para isso:

val f = { x: Pair<BigInteger, BigInteger> ->


Pair(x.segundo, x.primeiro + x.segundo) }
}

Ou usando uma declaração de desestruturação, ficaria assim:

val f = { (a, b): Pair<BigInteger, BigInteger> -> Pair(b, a + b)}

Para substituir a função recursiva por uma corecursiva, você precisará de duas
funções adicionais: map e iterate .
Exercício 4.16

Defina a iterate função que funciona como unfold , exceto que, em vez de
chamar a si mesma recursivamente até que uma condição seja atendida, ela
chama a si mesma um determinado número de vezes.

Dica

Comece com uma cópia da unfold função e altere o último parâmetro e a


condição.

Solução

Em vez de um predicado, a função recebe um número inteiro como terceiro


argumento:

fun <T> iterate(seed: T, f: (T) -> T, n: Int): List<T> {

A função usa uma função auxiliar tail-recursiva que é idêntica àquela usada por,
unfold exceto pela condição:

fun <T> iterate(seed: T, f: (T) -> T, n: Int): List<T> {


tailrec fun iterate_(acc: List<T>, seed: T): List<T> =
if (tamanho acc. < n)
iterate_(acc + seed, f(seed))
senão
acc
return iterate_(listOf(), seed)
}
Exercício 4.17

Defina uma map função que aplica uma função (T) -> U a cada elemento de a
List<T> , produzindo a List<U> .

Dica

Você pode definir uma função recursiva de cauda ou pode definir sua função em
termos de foldLeft ou foldRight . Uma boa ideia seria começar do copy fun-
ção que você criou ao definir reverse .

Solução

Uma solução explicitamente recursiva poderia ser

fun <T, U> map(lista: Lista<T>, f: (T) -> U): Lista<U> {


tailrec fun map_(acc: List<U>, list: List<T>): List<U> =
if (lista.isEmpty())
acc
senão
map_(acc + f(list.head()), list.tail())
return map_(listOf(), lista)
}

É mais simples e seguro reutilizar foldLeft porque abstrai a recursão. Lembre-


se da copy função:

fun <T> copy(lista: Lista<T>): Lista<T> =


foldLeft(list, listOf()) { lst, elem -> lst + elem}
Tudo o que você precisa fazer é aplicar o argumento da função a cada elemento
durante a cópia:

fun <T, U> mapa(lista: Lista<T>, f: (T) -> U): Lista<U> =


foldLeft(list, listOf()) { acc, elem -> acc + f(elem)}

Exercício 4.18

Defina uma versão corecursiva da função de Fibonacci produzindo uma string re-
presentando os n primeiros números de Fibonacci.

Solução

Você precisa iterar usando um par dos dois primeiros números e uma função que
calcula o próximo par a partir do anterior. Isto lhe dará uma lista de pares. Você
pode então mapear esta lista com uma função retornando o primeiro elemento de
cada par e converter a lista resultante em umacorda:

fun fiboCorecursive(número: Int): String {


val seed = Pair(BigInteger.ZERO, BigInteger.ONE)
val f = { x: Pair<BigInteger, BigInteger> -> Pair(x.second, x.first + x.second) }
val listOfPairs = iterate(seed, f, number + 1)
val list = map(listOfPairs) { p -> p.first }
return makeString(lista, ", ")
}
4.4.4 Usando memorização automática

Memorizaçãonão é usado apenas para funções recursivas. Pode ser usado para
acelerar qualquer função. Pense em como você realiza a multiplicação. Se você
precisar multiplicar 234 por 686, provavelmente precisará de uma caneta e papel
ou uma calculadora. Mas se lhe pedirem para multiplicar 9 por 7, você pode res-
ponder imediatamente, sem fazer nenhum cálculo. Isso ocorre porque você está
usando uma multiplicação memorizada.

Uma função memorizada funciona da mesma maneira, embora precise fazer o


cálculo uma vez para reter o resultado. Imagine que você tem uma função dou-
ble que multiplica seu argumento por 2:

divertido duplo(x: Int) = x * 2

Você pode memorizar esta função armazenando o resultado em um mapa. Veja


como isso pode ser feito usando técnicas de programação tradicionais envolvendo
condições de teste e controle do fluxo do programa:

val cache = mutableMapOf<Int, Int>() ①

divertido duplo(x: Int) =


if (cache.containsKey(x)) { ②
cache[x] ③
} senão {
val resultado = x * 2 ④
cache.put(x, resultado) ⑤
resultado ⑥
}

① Usa um mapa mutável para armazenar os resultados

② Consulta o mapa para ver se o resultado já foi computado

③ Se encontrado, retorna o resultado

④ Se não for encontrado, calcula o resultado

⑤ Coloca o resultado no mapa

⑥ Retorna o resultado

Acontece que neste caso particular, o fluxo de teste e controle já foram abstraídos
no computeIfAbsent função:

cache val: MutableMap<Int, Int> = mutableMapOf()

fun double(x: Int) = cache.computeIfAbsent(x) { it * 2 }

Ou você pode preferir uma função de valor como

val double: (Int) -> Int = { cache.computeIfAbsent(it) { it * 2 } }

Mas surgem dois problemas:


Você deve repetir esta modificação para todas as funções que deseja
memorizar.
O mapa que você usa é exposto ao exterior.

O segundo problema é fácil de resolver. Você pode colocar a função em um objeto


separado, incluindo o mapa, com acesso privado. Aqui está um exemplo no caso
de uma fun função:

objeto Duplicador {

cache val privado: MutableMap<Int, Int> = mutableMapOf()

fun double(x: Int) = cache.computeIfAbsent(x) { it * 2 }


}

Você pode usar esse objeto sempre que quiser calcular um valor:

val y = Doubler.double(x);

Com esta solução, o mapa deixa de ser acessível a partir do exterior. Agora você
resolveu o segundo problema. O que você pode fazer para resolver o primeiro?

Vamos começar com os requisitos. O que você precisa é uma maneira de fazer o
seguinte:

val f: (Int) -> Int = { it * 2 }


val g: (Int) -> Int = Memoizer.memoize(f)
Então você pode usar a função memoized como um substituto para o original. To-
dos os valores retornados pela função g são calculados pela função original f na
primeira vez e retornados do cache para todos os acessos subsequentes. Por outro
lado, se você criar uma terceira função

val f: (Int) -> Int = { it * 2 }


val g: (Int) -> Int = Memoizer.memoize(f)
val h: (Int) -> Int = Memoizer.memoize(f)

os valores armazenados em cache por g não serão retornados por h ; g e h usará


caches separados (a menos que você memorize a memoize função!).

A listagem a seguir mostra a implementação do Memoizer classe, que é bastante


simples.

Listagem 4.2 A classe Memoizer

class Memoizer<T, U> private constructor() {

cache de valor privado = ConcurrentHashMap<T, U>()

private fun doMemoize(função: (T) -> U): (T) -> U =


{ entrada ->
cache.computeIfAbsent(input) { ①
função(isso)
}
}

objeto complementar {
fun <T, U> memoize(função: (T) -> U): (T) -> U = ②
Memoizer<T, U>().doMemoize(function)
}
}

① Lida com o cálculo, chamando a função original se necessário

② Retorna uma versão memorizada de seu argumento de função

A listagem a seguir mostra como essa classe pode ser usada. O programa simula
uma longa computação para mostrar o resultado da memorização da função.

Listagem 4.3 Demonstrando o memoizer

fun longComputation(number: Int): Int { ①


Thread.sleep(1000) ②
número de retorno
}

fun main(args: Array<String>) {


val startTime1 = System.currentTimeMillis()
val resultado1 = longComputation(43)
val time1 = System.currentTimeMillis() - startTime1
val memoizedLongComputation =
Memoizer.memoize(::longComputation) ③
val startTime2 = System.currentTimeMillis()
val result2 = memoizedLongComputation(43)
val time2 = System.currentTimeMillis() - startTime2
val startTime3 = System.currentTimeMillis()
val result3 = memoizedLongComputation(43)
val time3 = System.currentTimeMillis() - startTime3
println("Chamada para função não memorizada: resultado = " +
"$resultado1, tempo = $tempo1")
println("Primeira chamada para função memorizada: resultado = " +
"$resultado2, tempo = $tempo2")
println("Segunda chamada para função não memorizada: resultado = " +
"$resultado3, tempo = $tempo3")
}

① A função para memorizar

② Simula uma computação longa

③ A função memorizada

A execução deste programa produz o seguinte resultado:

Chamada para função não memorizada: resultado = 43, tempo = 1000


Função memorizada da primeira chamada: resultado = 43, tempo = 1001
Segunda chamada para função não memorizada: resultado = 43, tempo = 0

Agora você pode criar funções memorizadas a partir das comuns chamando uma
única função em Memoizer , mas para usar essa técnica na produção, você teria
que lidar com possíveis problemas de memória. Esse código é aceitável se o nú-
mero de entradas possíveis for baixo, para que você possa manter todos os resul-
tados na memória sem causar estouro de memória. Caso contrário, você pode
usar referências suaves ou referências fracas para armazenarvalores.
4.4.5 Implementando memoização de funções multi-argumentais

ComoEu disse antes, não existe no mundo uma função com vários argumentos. As
funções representam relacionamentos entre um conjunto (o conjunto de origem)
e outro conjunto (o conjunto de destino). Eles não podem ter vários argumentos.
Funções que parecem ter vários argumentos são

Funções de tuplas
Funções retornando funções retornando funções ... retornando um resultado

Em ambos os casos, você está preocupado apenas com funções de um argumento,


portanto, pode usar facilmente seu Memoizer objeto.

Usar funções de tuplas pode parecer a escolha mais simples. Você poderia usar
o Pair ou Triple classes oferecidas pelo Kotlin, ou você pode definir as suas pró-
prias se precisar agrupar mais de três elementos. A segunda opção é muito mais
fácil, mas você precisa usar a versão ao curry das funções como fez na seção
“Funções ao curry” (capítulo 3).

Memorizar funções curried é fácil, embora você não possa usar a mesma forma
simples de antes. Você tem que memorizar cada função:

val mhc = Memoizer.memoize { x: Int ->


Memoizer.memoize { y: Int ->
x + y
}
}

Você pode usar a mesma técnica para memorizar uma função de três argumentos:
val f3 = { x: Int -> { y: Int -> { z: Int -> x + y - z } } }

val f3m = Memoizer.memoize { x: Int ->


Memoizer.memoize { y: Int ->
Memoizer.memoize { z: Int -> x + y - z }
}
}

A listagem a seguir mostra um exemplo de teste da função memorizada com três


argumentos.

Listagem 4.4 Testando uma função memorizada com três argumentos

val f3m = Memoizer.memoize { x: Int ->


Memoizer.memoize { y: Int ->
Memoizer.memoize { z: Int ->
longComputation(z) - (longComputation(y) + longComputation(x))
}
}
}

fun main(args: Array<String>) {


val startTime1 = System.currentTimeMillis()
val resultado1 = f3m(41)(42)(43)
val time1 = System.currentTimeMillis() - startTime1
val startTime2 = System.currentTimeMillis()
val resultado2 = f3m(41)(42)(43)
val time2 = System.currentTimeMillis() - startTime2
println("Primeira chamada para função memorizada: resultado = " +
"$resultado1, tempo = $tempo1")
println("Segunda chamada para função memorizada: resultado = " +
"$resultado2, tempo = $tempo2")
}

Este programa produz a seguinte saída:

Primeira chamada para a função memorizada: resultado = -40, tempo = 3003


Segunda chamada para a função memorizada: resultado = -40, tempo = 0

Essa saída mostra que o primeiro acesso à função memorizada levou 6.786 milis-
segundos e o segundo retornou imediatamente.

Por outro lado, usar uma função de uma tupla pode parecer mais fácil depois de
definir a Tuple classe porque você pode usar um data class para o qual o Ko-
tlin fornecerá equals e hashCode funções automaticamente. O exemplo a seguir
mostra a implementação de Tuple4 (se você precisar apenas de Tuple2 ou
Tuple3 , poderá usar as classes Pair ou ): Triple

classe de dados Tuple4<T, U, V, W>(val primeiro: T,


val segundo: U,
Val terceiro: V,
val quarto: W)

A listagem a seguir mostra um exemplo de teste de uma função memorizada to-


mando Tuple4 comoEstáargumento.

Listagem 4.5 Uma função memorizada de um Tuple4


val ft = { (a, b, c, d): Tuple4<Int, Int, Int, Int> ->
longComputation(a) + longComputation(b)
- longComputation(c) * longComputation(d) }

val ftm = Memoizer.memoize(ft)

fun main(args: Array<String>) {


val startTime1 = System.currentTimeMillis()
val resultado1 = ftm(Tuple4(40, 41, 42, 43))
val time1 = System.currentTimeMillis() - startTime1
val startTime2 = System.currentTimeMillis()
val resultado2 = ftm(Tuple4(40, 41, 42, 43))
val time2 = System.currentTimeMillis() - startTime2
println("Primeira chamada para função memorizada: resultado = " +
"$resultado1, tempo = $tempo1")
println("Segunda chamada para função memorizada: resultado = " +
"$resultado2, tempo = $tempo2")
}

4.5 As funções memorizadas são puras?

memorizandotrata-se de manter o estado entre as chamadas de função. O com-


portamento de uma função memorizada depende do estado atual, mas sempre re-
tornará o mesmo valor para o mesmo argumento. Apenas o tempo necessário
para retornar o valor será diferente. A função memorizada ainda é uma função
pura se a função original for pura.

Uma variação no tempo pode ser um problema. Uma função como a função origi-
nal de Fibonacci que precisa de muitos anos para ser concluída pode ser chama-
danon-terminating , portanto, um aumento no tempo pode criar um problema.
Por outro lado, tornar uma função mais rápida não deve ser um problema real. Se
for, há um problema muito maiorEm outro lugar!

Resumo

Uma função recursiva chama a si mesma, usando essa chamada para si mesma
como um elemento para computação posterior.
As funções recursivas empurram o estado de computação atual para a pilha
antes de se chamarem recursivamente.
O tamanho da pilha padrão do Kotlin é limitado. Se o número de etapas recur-
sivas for muito alto, você obterá um arquivo StackOverflowException .
Funções recursivas de cauda são funções nas quais a chamada recursiva está
na última posição ( cauda ).
No Kotlin, as funções recursivas de cauda são otimizadas usando a eliminação
de chamada de cauda (TCE).
Lambdas podem ser feitos recursivos.
A memoização permite que as funções se lembrem do resultado calculado
para acelerar o acesso posterior.
A memoização pode ser automática.

Você também pode gostar