Você está na página 1de 72

9Trabalhando com preguiça

Nissocapítulo

Entenda a importância da preguiça


Implementando preguiça em Kotlin
Compondo com preguiça
Criando uma estrutura de dados de lista preguiçosa
Manipulando fluxos infinitos

Imagine que você é o barman de um barzinho que oferece dois produtos: café ex-
presso e suco de laranja. Um cliente entra e senta. Você tem duas opções:

1. Você prepara um espresso, espreme um suco de laranja, leva os dois até o cli-
ente e deixa ele escolher.
2. Você pergunta ao cliente o que ele gostaria de comer, prepara e serve.

Em termos de programação, no segundo caso, você está sendo preguiçoso , en-


quanto no primeiro caso está sendo rígido . A escolha da melhor estratégia de-
pende de você. Este capítulo não é sobre moral, mas sobre eficiência.

Neste caso, existem outras opções. Você poderia preparar ansiosamente um café
expresso e um suco de laranja antes de qualquer cliente entrar. Quando um cli-
ente chegar, pergunte à pessoapara escolher o que querem, servir o que é solici-
tado e imediatamente fazer uma bebida em preparação para o próximo cliente.
Parece loucura? Não tão.
Imagine que você também está oferecendo tortas de maçã e tortas de morango.
Você esperaria um cliente entrar e escolher um tipo de torta para prepará-la? Ou
você prepararia ansiosamente um de cada tipo? Se você escolher ser preguiçoso,
esperaria que o cliente escolhesse, digamos, torta de maçã. Então você preparava
uma torta de maçã, cortava uma fatia e levava para o cliente. Essa seria a aborda-
gem preguiçosa, embora você tivesse preparado ao mesmo tempo as outras fatias
de torta de maçã.

As linguagens de programação funcionam da mesma forma. E, como no exemplo,


o modo normal geralmente é tão óbvio que ninguém sequer pensa nisso, a menos
que surja um problema. Na primeira parte deste capítulo, comparo rigidez e pre-
guiça. Embora Kotlin seja uma linguagem estrita, você ainda pode usar a pre-
guiça. No restante do capítulo, ensinarei várias habilidades relacionadas à pre-
guiça em Kotlin: como implementar a preguiça, como compor com preguiça,
como criar uma estrutura de dados de lista preguiçosa e, finalmente, como lidar
com fluxos infinitos.

9.7 Rigidez versus preguiça

Algumas línguas são consideradas preguiçosas; outros são rigorosos. Geralmente,


trata-se de como as linguagens avaliam argumentos de funções ou métodos. Mas,
na realidade, todas as linguagens são preguiçosas porque a preguiça é a verda-
deira essência da programação.

A programação consiste em compor as instruções do programa que serão avalia-


das quando o programa for executado. Se o rigor total fosse aplicado, cada instru-
ção que você escrevesse seria executada assim que você pressionasse a tecla En-
ter, que é, de fato, o que acontece quando você usa um REPL (Read, Eval, Print
Loop).
Deixando de lado o fato de que um programa é basicamente uma construção pre-
guiçosa, os programas escritos em linguagens estritas como Kotlin ou Java são
uma composição de elementos estritos? Certamente não. Linguagens estritas são
estritas em relação à avaliação de argumento de método/função, mas a maioria
das outras construções são preguiçosas. Veja o exemplo de uma if...else estru-
tura:

val resultado = if (testCondition())


getIfTrue()
senão
getIfFalse()

Obviamente, a testCondition funçãosempre será avaliado. Mas apenas um dos


getIfTrue e getIfFalse funções serão chamadas, dependendo do valor retor-
nado por testCondition . Embora a if...else estrutura seja rígida em rela-
ção à condição, é preguiçosa em relação aos dois ramos. Se getIfTrue e getIf-
False fossem efeitos, você não poderia fazer nada útil com um totalmente estrito
if...else porque ambos os efeitos seriam executados em qualquer caso. Pelo
contrário, se fossem funções, isso não mudaria o resultado do programa. Ambos
os valores seriam computados, mas apenas aquele correspondente à condição se-
ria retornado. Isso seria uma perda de tempo de processamento, mas ainda seria
utilizável.

Rigor e preguiça se aplicam não apenas a estruturas de controle e argumentos de


função, mas a tudo na programação. Por exemplo, considere a seguinte
declaração:

val x: Int = 2 + 3
Aqui, x é imediatamente avaliado como 5 . Como o Kotlin é uma linguagem es-
trita, ele executa a adição imediatamente. Vejamos outro exemplo:

val x: Int = getValue()

Em Kotlin, assim que a x referência é declarada, a getValue funçãoé chamado


para fornecer o valor correspondente. Por outro lado, com uma linguagem pre-
guiçosa, a getValue função só seria chamada se e quando o valor referenciado
por x fosse usado. Isso pode fazer uma grande diferença. Por exemplo, observe o
seguinte programa:

fun main(args: Array<String>) {


val x = getValor()
}

fun getValue(): Int {


println("Retornando 5")
retornar 5
}

Este programa é impresso Returning 5 no console porque a getValue fun-


çãoserá chamado, embora o valor retornado nunca seja usado. Em uma lingua-
gem preguiçosa, nada seria avaliado, então nada seria impresso noconsole.

9.8 Kotlin e rigidez

KotlinGenericNameé suposto ser uma linguagem estrita. Tudo é avaliado imedia-


tamente. Argumentos de função são passados ​por value , o que significa que eles
são avaliados primeiro e então o valor avaliado é passado. Por outro lado, em lin-
guagens preguiçosas, diz-se que os argumentos são passados ​por nome , o que sig-
nifica não avaliado. Não se confunda com o fato de que os argumentos de função
em Kotlin geralmente são referências. As referências são endereços e esses ende-
reços são passados ​por valor.

Algumas linguagens são rígidas (como Kotlin e Java) e outras são preguiçosas; al-
guns são rígidos por padrão e opcionalmente preguiçosos, e outros são preguiço-
sos por padrão e opcionalmente rígidos. Kotlin, no entanto, nem sempre é rigo-
roso. Estas são algumas das construções preguiçosas do Kotlin:

operadores booleanos || e &&


if ... else
for ciclo
while ciclo

Kotlin também oferece construções preguiçosas, como Sequence , bem como os


meios para tornar a avaliação de propriedade preguiçosa, como você verá em
breve.

Boolean Considere os operadores Kotlin || e && . Essesos operadores não avali-


arão seus operandos se não for necessário para computar o resultado. Se o pri-
meiro operando de || for true , o resultado será true , qualquer que seja o se-
gundo operando, então não há necessidade de avaliá-lo. Da mesma forma, se o
primeiro operando do && operador for false , o resultado será false , qualquer
que seja o segundo operando. Novamente, não há necessidade de avaliá-lo.

Isso te lembra algo? No capítulo 8, você criou uma versão especial da fol-
dLeft funçãoque poderia escapar do cálculo quando um elemento absorvente
(também chamado de zero elemento ) foi encontrado. Aqui, false é um elemento
absorvente para a && operação e true é um elemento absorvente para a || ope-
ração.

Imagine que você deseja simular operadores booleanos com funções. A lista a se-
guir mostra o que você pode fazer.

Listagem 9.1 As funções lógicas and e or

fun main(args: Array<String>) {


println(ou(verdadeiro, verdadeiro))
println(ou(verdadeiro, falso))
println(ou(falso, verdadeiro))
println(ou(falso, falso))

println(e(verdadeiro, verdadeiro))
println(e(verdadeiro, falso))
println(e(falso, verdadeiro))
println(e(falso, falso))
}

fun or(a: Boolean, b: Boolean): Boolean = if (a) true else b

fun and(a: Boolean, b: Boolean): Boolean = if (a) b else false

Usar os operadores booleanos fornece uma maneira mais simples de fazer isso,
mas seu objetivo aqui é evitar esses operadores. Você terminou? A execução desse
programa exibe o seguinte resultado no console:

verdadeiro
verdadeiro
verdadeiro
falso
verdadeiro
falso
falso
falso

Até agora tudo bem. Mas agora tente executar o programa mostrado na listagem a
seguir.

Listagem 9.2 O problema com rigidez

fun main(args: Array<String>) {


println(getFirst() || getSecond())
println(ou(getFirst(), getSecond()))
}

fun getFirst(): Boolean = true

divertido getSecond(): Booleano = lançar IllegalStateException()

fun or(a: Boolean, b: Boolean): Boolean = if (a) true else b

fun and(a: Boolean, b: Boolean): Boolean = if (a) b else false

Este programa imprime o seguinte:

verdadeiro
Exceção no encadeamento "principal" java.lang.IllegalStateException

Obviamente, a or funçãonão é equivalente ao || operador. A diferença é que


|| avalia seu operando preguiçosamente, o que significa que o segundo ope-
rando não é avaliado se o primeirouma é true porque é desnecessário para com-
putar o resultado. Mas a or função avalia seus argumentos estritamente, o que
significa que o segundo argumento é avaliado mesmo que seu valor não seja ne-
cessário, então o IllegalStateException é sempre lançado. Nos capítulos 6 e 7,
você encontrou esse problema com a getOrElse função porque seu argumento
era sempre avaliado, mesmo que o cálculo fossebem sucedido.

9.9 Kotlin e preguiça

Preguiçaé necessário em muitas ocasiões. Kotlin, de fato, usa laziness para cons-
truções como if...then , loops e try...catch blocos. Sem isso, um cat-
ch bloco, por exemplo, seria avaliado mesmo na ausência de uma exceção.

A implementação da preguiça é essencial quando se trata de fornecer comporta-


mento para erros, bem como quando você precisa manipular estruturas de dados
infinitas. Kotlin fornece uma maneira de implementar a preguiça por meio do uso
de um delegado. Veja como funciona

val primeiro: Boolean por Delegate ()

onde Delegate é uma classe que implementa a seguinte função:

operador fun getValue(thisRef: Qualquer?, propriedade: KProperty<*>): Booleano

Observe duas coisas importantes:

1. a Delegate classe, que pode ter qualquer nome que você escolher, não pre-
cisa implementar nenhuma interface. Deve declarar e implementar a função
anterior, que será chamada por reflexão.
2. Se você estivesse declarando a var em vez de a val , a Delegate classetam-
bém deve implementar a função correspondente para definir o arquivo
value:operator fun setValue(thisRef: Any?, property:
KProperty<*>, value: Boolean) .

Kotlin também fornece delegados padrão, entre os quais Lazy podem ser usados ​
para implementar preguiça:

val primeiro: Booleano por preguiçoso { ... }

Esta é uma abreviação para usar algo como

classe Preguiçoso {
operador fun getValue(thisRef: Any?,
propriedade: KProperty<*>): Booleano = ...
}

val primeiro: Boolean por Lazy ()

A interface Kotlin padrão Lazy é, no entanto, um pouco mais sofisticada. Usando


essa técnica, o exemplo que usa funções para implementar o or operador boolea-
notorna-se isto:

fun main(args: Array<String>) {


val primeiro: Boolean por lazy { true }
val segundo: Boolean por lazy { throw IllegalStateException() }
println(primeiro || segundo)
println(ou(primeiro, segundo))
}
fun or(a: Boolean, b: Boolean): Boolean = if (a) true else b

Infelizmente, isso não é preguiça de verdade, como você pode ver executando este
programa:

verdadeiro
Exceção no encadeamento "principal" java.lang.IllegalStateException

O que está acontecendo aqui é que as funções usadas para inicializar first e
second não são chamadas quando essas referências são declaradas; isso é pre-
guiça normal. Mas as funções são chamadas quando a or funçãorecebe os argu-
mentos first e second em vez de quando eles são realmente necessários, o que
faria com second que nunca fosse chamado porque nunca seria necessário. Isso
não é preguiça detudo!

implementações de preguiça

Implementandoa preguiça em Kotlin da mesma forma que é implementada em


linguagens preguiçosas não é totalmente possível. Mas você pode atingir o mesmo
objetivo usando os tipos certos. Pense no que você fez nos capítulos 5, 6 e 7. No ca-
pítulo 7, você aprendeu a resumir listas em dois conceitos relacionados — o tipo
dos elementos e outra coisa, representada pelo List tipo. Esse “algo mais” era
um conceito composto que abrangia a cardinalidade (pode haver qualquer nú-
mero de elementos, incluindo 0, 1 ou mais), bem como o fato de que esses elemen-
tos eram ordenados. Você poderia ter levado a abstração ainda mais distinguindo
cardinalidade e ordem. Mas vamos pensar no conceito de List como um todo. O
resultado foi o tipo parametrizado List<A> .
No capítulo 6, você aprendeu a resumir o conceito de opcionalidade em dois con-
ceitos relacionados: o tipo dos dados (vamos chamá-los de A ) e o Option tipo. E
no capítulo 7, você aprendeu a abstrair a possibilidade de erro em outro tipo com-
posto, Result<A> . Como você pode ver, há um padrão. De um lado, você tem um
tipo simples A e, do outro, um tipo de modalidade aplicada a esse tipo simples. A
preguiça pode ser considerada outra modalidade, então certamente você pode im-
plementá-la como um tipo. Vamos chamá-lo Lazy<A> .

Você poderia objetar que a preguiça pode ser representada de uma maneira
muito mais simples usando uma função constante. Como você sabe, por já ter
usado esse conceito nos capítulos anteriores, uma função constante é uma função
que recebe qualquer argumento de qualquer tipo e sempre retorna o mesmo va-
lor. Tal função pode ter o seguinte tipo:

() -> Int

Aqui a função retorna um arquivo Int . Para criar um valor inteiro preguiçoso,
você pode escrever

val x: () -> Int = { 5 }

Isso é bobagem porque 5 é um literal e não faz sentido inicializar preguiçosa-


mente uma referência a um valor literal. Mas se você voltar ao nosso exemplo an-
terior usando Booleanos preguiçosos, obterá isto:

fun main(args: Array<String>) {


val primeiro = { verdadeiro }
val segundo = { lançar IllegalStateException() }
println(primeiro() || segundo())
println(ou(primeiro, segundo))
}

fun or(a: () -> Boolean, b: () -> Boolean): Boolean = if (a()) true else b()

Isso funciona como esperado, produzindo

verdadeiro
verdadeiro

Uma diferença com laziness como ele é implementado em linguagens preguiçosas


é que você mudou os tipos. Um preguiçoso A não é um A , então a or funçãoteve
que ser modificado para se adequar aos novos tipos.

Mas há uma diferença muito mais importante com a verdadeira preguiça: se você
usar o valor duas vezes, a função será chamada duas vezes. Calcular o valor é
uma operação demorada. Isso desperdiça tempo do processador. É o que chama-
mos de chamada por avaliação de nome, o que significa que o valor não é avali-
ado antes de ser necessário, mas é avaliado sempre que necessário! Você poderia
obter o mesmo resultado usando uma fun função, mas você teria que fornecer o
tipo de retorno para a função lançando uma exceção porque o Kotlin não pode in-
feri-lo:

fun main(args: Array<String>) {


diversão primeiro() = verdadeiro
fun second(): Boolean = throw IllegalStateException()
println(primeiro() || segundo())
println(ou(::primeiro, ::segundo))
}
fun or(a: () -> Boolean, b: () -> Boolean): Boolean = if (a()) true else b()

Você consegue pensar em uma solução para resolver esse problema e chamar a
função apenas na primeira vez que for necessário? Esse tipo de avaliação é cha-
mado de chamada por necessidade . Se você se lembra do que aprendeu no capí-
tulo 4, sabe que a resposta é memoização.

Exercício 9.1

Implemente um Lazy<A> tipo que funcione como uma função memorizada () -


> A . Deve ser utilizável no seguinte exemplo:

fun main(args: Array<String>) {


val primeiro = Preguiçoso {
println("Avaliando primeiro")
verdadeiro
}
val segundo = Preguiçoso {
println("Avaliando segundo")
lançar IllegalStateException ()
}
println(primeiro() || segundo())
println(primeiro() || segundo())
println(ou(primeiro, segundo))
}

fun or(a: Lazy<Boolean>, b: Lazy<Boolean>): Boolean = if (a()) true else b()

E isso deve produzir o seguinte resultado:


avaliando primeiro
verdadeiro
verdadeiro
verdadeiro

Dica

Se você fizer o Lazy tipo estender a função () -> A , deve funcionar bem. Isso
não é obrigatório, mas simplificará o uso do tipo. Use a by lazy construção para
memoização e evite a mutação de estado explícita.

Solução

Aqui está a solução baseada na by lazy construção:

class Preguiçoso<saída A>(função: () -> A): () -> A {

valor val privado: A por lazy(function)

operador override fun invoke(): A = valor


}

Como você pode ver, esta implementação não faz uso de mutação de estado. A
mutação de estado é abstraída na by lazy construção fornecida pelo Kotlin. No
capítulo 14, você aprenderá a usar a mesma técnica para abstrair o estado mutá-
vel de compartilhamento.
9.10.1 Compondo valores preguiçosos

DentroNo exemplo anterior, você compôs valores booleanos preguiçosos usando


a or função:

fun or(a: Lazy<Boolean>, b: Lazy<Boolean>): Boolean = if (a()) true else b()

Mas isso foi trapaça porque você estava confiando na preguiça da if...else ex-
pressão para evitar avaliar o segundo argumento. Imagine uma função compondo
duas strings:

fun constructMessage(saudações: String,


nome: String): String = "$saudações, $nome!"

Agora imagine que a aquisição de cada parâmetro é uma tarefa que consome
muitos recursos, e você deseja fornecer o resultado de constructMessage forma
preguiçosa, para que possa ser usado ou não dependendo de uma condição ex-
terna. Se essa condição não for atendida, as strings name e não devem ser adqui-
ridas. Para o exemplo, digamos que a condição é que um número inteiro aleatório
seja par. Veja como deve funcionar: greetings

saudações val = preguiçoso {


println("Avaliando saudações")
"Olá"
}
nome do val: Preguiçoso<String> = Preguiçoso {
println("nome de computação")
"Mickey"
}
val message = constructMessage(saudações, nome)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) <compõe e imprime a mensagem>
else "Sem saudações quando o tempo é estranho")

Agora você tem que refatorar a constructMessage funçãopara que ele use argu-
mentos avaliados preguiçosamente. Você poderia usar o seguinte:

fun constructMessage(saudações: Lazy<String>,


nome: Lazy<String>): String =
"${saudações()}, ${nome()}!"

Não haveria nenhum benefício real em usar valores preguiçosos aqui porque eles
seriam avaliados antes de serem concatenados ao chamar a constructMessa-
ge função, mesmo que a condição não tenha sido atendida. O que seria útil é uma
constructMessage função que retornaria um resultado não avaliado, ou seja,
Lazy<String> sem avaliar nenhum dos parâmetros.

Exercício 9.2

Escreva uma versão preguiçosamente avaliada da constructMessage função.

Dica

Verifique se cada parte da mensagem é avaliada apenas uma vez, qualquer que
seja o número de chamadas à função.
Solução

Aqui está uma função executando uma concatenação preguiçosa dos dois argu-
mentos, juntamente com o teste para várias avaliações:

fun constructMessage(saudações: Lazy<String>,


nome: Preguiçoso<String>): Preguiçoso<String> =
Preguiçoso { "${greetings()}, ${name()}!" }

fun main(args: Array<String>) {


saudações val = preguiçoso {
println("Avaliando saudações")
"Olá"
}
val nome1: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome")
"Mickey"
}
val nome2: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome")
"Donald"
}
val defaultMessage = Preguiçoso {
println("Avaliando mensagem padrão")
"Sem saudações quando o tempo é estranho"
}
val message1 = constructMessage(saudações, nome1)
val message2 = constructMessage(saudações, nome2)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) message1() else defaultMessage())
println(if (condição) message1() else defaultMessage())
println(if (condição) message2() else defaultMessage())
}
Você deve executar o teste várias vezes. Quando a condição for atendida, você
obtém

Avaliando saudações
Avaliando nome
Olá, Mickey!
Olá, Mickey!
Avaliando nome
Olá, Donald!

Como você pode ver, o greeting argumento é avaliado apenas uma vez, e o na-
me argumentoé avaliada duas vezes (com dois valores diferentes), mesmo que a
função seja chamada três vezes.

OBSERVAÇÃO Isso não é absolutamente exato. Do ponto de vista da função,


os argumentos são avaliados cada vez que a função é chamada como instâncias
de Lazy , mas apenas a primeira vez faz com que as Lazy instâncias sejam avali-
adas posteriormente.

Quando a condição não for atendida, você obtém o seguinte:

Avaliando a mensagem padrão


Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho

Como você pode ver, apenas a mensagem padrão é avaliada.

A condição deve ser externa , o que significa que não envolve dados preguiçosos.
Caso contrário, os dados teriam que ser avaliados para testar a condição. Também
é possível definir val funções curried preguiçosas. Você aprendeu sobre funções
curried no capítulo 3.

Exercício 9.3

Escreva uma val versão preguiçosa ao curry do constructMessage função.

Solução

Nenhuma dificuldade real aqui:

val constructMessage: (Lazy<String>) -> (Lazy<String>) -> Lazy<String> =


{ saudações ->
{ nome ->
Preguiçoso { "${greetings()}, ${name()}!" }
}
}

fun main(args: Array<String>) {


saudações val = preguiçoso {
println("Avaliando saudações")
"Olá"
}
val nome1: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome")
"Mickey"
}
val nome2: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome")
"Donald"
}
val message1 = constructMessage(saudações)(nome1)
val message2 = constructMessage(saudações)(nome2)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) message1() else defaultMessage())
println(if (condição) message2() else defaultMessage())
}

O resultado é exatamente o mesmo, mas a sintaxe para chamar a função teve que
ser adaptada. Este é um bom exemplo do uso de funções curry. Você pode precisar
cumprimentar muitas pessoas com a mesma mensagem, mas com um nome dife-
rente. Isso poderia então ser implementadoComo

val constructMessage: (Lazy<String>) -> (Lazy<String>) -> Lazy<String> =


{ saudações ->
{ nome ->
Preguiçoso { "${greetings()}, ${name()}!" }
}
}

fun main(args: Array<String>) {


saudações val = preguiçoso {
println("Avaliando saudações")
"Olá"
}
val nome1: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome1")
"Mickey"
}
val nome2: Preguiçoso<String> = Preguiçoso {
println("Avaliando nome2")
"Donald"
}
val defaultMessage = Preguiçoso {
println("Avaliando mensagem padrão")
"Sem saudações quando o tempo é estranho"
}
val saudaçãoString = constructMessage(saudações)
val mensagem1 = saudaçãoString(nome1)
val mensagem2 = saudaçãoString(nome2)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) message1() else defaultMessage())
println(if (condição) message2() else defaultMessage())
}

9.10.2 Funções de elevação

Muitas vezes,você terá uma função trabalhando em alguns valores avaliados e de-
sejará usar essa função com valores não avaliados sem causar avaliação. Afinal,
essa é a essência da programação.

Exercício 9.4

Crie uma função que tome como argumento uma função curried de dois argu-
mentos avaliados e retorne a função correspondente para um argumento não
avaliado que produz o mesmo resultado, mas não avaliado. Dada a função

val consMessage: (String) -> (String) -> String =


{ saudações ->
{ nome ->
"$saudações, $nome!"
}
}

escreva uma função chamada lift2 que produza a seguinte função (sem avaliar
nenhum argumento):
(Preguiçoso<String>) -> (Preguiçoso<String>) -> Preguiçoso<String>

Dica

Coloque esta função no Lazy objeto complementar para evitar conflito com outra
implementação de lift2 . Como alternativa, você pode declarar essa função no
nível do pacote e alterar seu nome, por exemplo, para liftLazy2 .

Solução

Primeiro, escreva a assinatura da função. a lift2 funçãotoma como argumento


uma função do tipo (String) -> (String) -> String . Este tipo de argu-
mento deve ser colocado entre parênteses e seguido por uma seta conforme mos-
trado aqui:

((String) -> (String) -> String) ->

Em seguida, escreva o tipo de retorno desta função:

((String) -> (String) -> String) -> (Preguiçoso<String>) -> (Preguiçoso<String>)


-> Preguiçoso<String>

Adicione o val lift2: à esquerda desta expressão e o sinal de igual à direita:

val elevador2: ((String) -> (String) -> String) -> (Lazy<String>) ->
(Preguiçoso<String>) -> Preguiçoso<String> =

Agora escreva a implementação. Como deveria produzir uma função do tipo


(Preguiçoso<String>) -> (Preguiçoso<String>) -> Preguiçoso<String>,

você pode começar com

{ f -> { ls1 -> { ls2 -> TODO() } } },

onde f é do tipo (String) -> (String) -> String) (a função para levantar)
e ls1 e ls2 são do tipo Lazy<String> :

val elevador2: ((String) -> (String) -> String) -> (Lazy<String>) ->
(Preguiçoso<String>) -> Preguiçoso<String> =
{f->
{ ls1 ->
{ ls2 ->
PENDÊNCIA()
}
}
}

O resto agora é simples. Você tem uma função curried de dois String argumen-
tos e duas instâncias de Lazy<String> . Obtenha os valores e aplique a função.
Como você deseja obter um Lazy<String> , crie uma Lazy instância do
resultado:

val elevador2: ((String) -> (String) -> String) -> (Lazy<String>) ->
(Preguiçoso<String>) -> Preguiçoso<String> =
{f->
{ ls1 ->
{ ls2 ->
Preguiçoso { f(ls1())(ls2()) }
}
}
}

Exercício 9.5

Generalize esta função para trabalhar com qualquer tipo. Desta vez, escreva uma
fun funçãono nível do pacote.

Solução

Comece com a assinatura. Como a função deve funcionar com qualquer tipo, ela
deve ser parametrizada com A, B, C , e o parâmetro (exclusivo) é uma função
do tipo (A) -> (B) -> C . O tipo de retorno é uma função do tipo (Lazy<A>)
-> (Lazy<B>) -> Lazy<C> :

fun <A, B, C> lift2(f: (A) -> (B) -> C): (Preguiçoso<A>) -> (Preguiçoso<B>) -> Preguiçoso<C

A implementação é semelhante à do exercício 4, exceto que o primeiro parâmetro


(a função) não é mais especificado porque é o parâmetro da fun funçãoe os tipos
sãodiferente:

fun <A, B, C> lift2(f: (A) -> (B) -> C):


(Preguiçoso<A>) -> (Preguiçoso<B>) -> Preguiçoso<C> =
{ ls1 ->
{ ls2 ->
Preguiçoso { f(ls1())(ls2()) }
}
}
9.10.3 Mapeamento e flatMapping Lazy

Eu estoucerteza de que agora você entende que Lazy é outro contexto computaci-
onal como List , Option , e Result (e muitos outros que você descobrirá mais
tarde), e você está tentado a fazê map - flatMap lo!

Exercício 9.6

Escreva uma map funçãopara aplicar uma função (A) -> B a a Lazy<A> , retor-
nando a Lazy<B> .

Dica

Defina-o como uma instânciafunção na Lazy classe.

Solução

Tudo o que você precisa fazer é aplicar a função de mapeamento ao valor lento.
Para não acionar a avaliação, envolva-a em um novo Lazy :

fun <B> map(f: (A) -> B): Preguiçoso<B> = Preguiçoso{ f(valor) }

Isso pode ser testado com o seguinte programa:

fun main(args: Array<String>) {

val cumprimenta: (String) -> String = { "Olá, $it!" }

nome do val: Preguiçoso<String> = Preguiçoso {


println("Avaliando nome")
"Mickey"
}
val defaultMessage = Preguiçoso {
println("Avaliando mensagem padrão")
"Sem saudações quando o tempo é estranho"
}
val mensagem = nome.map(cumprimenta)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) message() else defaultMessage())
println(if (condição) message() else defaultMessage())
}

Se a condição for válida, este programa imprime o seguinte, mostrando que os da-
dos são avaliados apenas uma vez, mesmo que sejam usados ​duas vezes:

Avaliando nome
Olá, Mickey!
Olá, Mickey!

Se a condição não for válida, a saída do programa mostra que a mensagem pa-
drão é avaliada apenas uma vez:

Avaliando a mensagem padrão


Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho

Exercício 9.7

Escreva uma flatMap função para aplicar uma função (A) -> Lazy<B> a a
Lazy<A> , retornando a Lazy<B> .
Dica

Defina-o como uma função de instância na Lazy classe.

Solução

Tudo o que você precisa fazer é aplicar a função de mapeamento ao valor lento e
à avaliação do gatilho. Mas como você não quer que isso aconteça até que seja ne-
cessário, embrulhe tudo de novo Lazy :

fun <B> flatMap(f: (A) -> Lazy<B>): Lazy<B> = Lazy { f(value)() }

Isso pode ser testado com o seguinte programa:

fun main(args: Array<String>) {

// Imagine que getGreetings é uma função cara com o efeito colateral


// de imprimir "Avaliando saudações" para o console
saudações val: Lazy<String> = Lazy { getGreetings(Locale.US) }

val flatGreets: (String) -> Lazy<String> =


{ nome -> saudações.map { "$it, $name!"} }

nome do val: Preguiçoso<String> = Preguiçoso {


println("nome de computação")
"Mickey"
}
val defaultMessage = Preguiçoso {
println("Avaliando mensagem padrão")
"Sem saudações quando o tempo é estranho"
}
val message = name.flatMap(flatGreets)
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) message() else defaultMessage())
println(if (condição) message() else defaultMessage())
}

Se a condição for válida, este programa imprime o seguinte, mostrando que o


nome e a mensagem de saudação são avaliados apenas uma vez, mesmo que se-
jam usados ​duas vezes:

Avaliando nome
Avaliando saudações
Olá, Mickey!
Olá, Mickey!

Se a condição não for válida, a saída do programa mostra que apenas a mensa-
gem padrão é avaliada e apenasuma vez:

Avaliando a mensagem padrão


Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho

9.10.4 Compondo Lazy com List

o Lazy O tipo pode ser composto com outros tipos que você desenvolveu nos capí-
tulos anteriores. Uma das operações mais comuns é transformar a
List<Lazy<A>> em a Lazy<List<A>> , para que a lista possa ser composta pre-
guiçosamente com funções de A . Esse tipo de composição deve ser feito sem ava-
liar os dados.
Exercício 9.8

Definir uma sequence funçãocom a seguinte assinatura:

fun <A> sequence(lst: List<Lazy<A>>): Lazy<List<A>>

Esta será uma fun funçãodefinido no nível do pacote.

Solução

Mais uma vez, a solução é simples. Você precisa mapear a lista com uma função
avaliando o conteúdo de cada elemento. Mas porque a sequence funçãonão deve
avaliar nada, envolva esta operação em uma nova Lazy instância:

fun <A> sequence(lst: List<Lazy<A>>): Lazy<List<A>> =


Preguiçoso { lst.map { it() } }

Aqui está um programa de exemplo mostrando a saída desta função:

fun main(args: Array<String>) {

val nome1: Preguiçoso<String> = Preguiçoso {


println("Avaliando nome1")
"Mickey"
}

val nome2: Preguiçoso<String> = Preguiçoso {


println("Avaliando nome2")
"Donald"
}
val nome3 = Preguiçoso {
println("Avaliando nome3")
"Pateta"
}
val lista = sequence(Lista(nome1, nome2, nome3))
val defaultMessage = "Sem saudações quando o tempo é ímpar"
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) list() else defaultMessage)
println(if (condição) list() else defaultMessage)
}

Novamente, dependendo de uma condição externa, o resultado (não avaliado) ou


uma mensagem padrão é exibida duas vezes. Quando a condição é válida, o resul-
tado é impresso duas vezes, produzindo uma única avaliação de todos os
elementos:

Avaliando nome1
Avaliando nome2
Avaliando nome3
[Mickey, Donald, Pateta, NIL]
[Mickey, Donald, Pateta, NIL]

Quando a condição não é válida, nada éavaliado:

Sem saudações quando o tempo é estranho


Sem saudações quando o tempo é estranho
9.10.5 Lidando com exceções

QuandoSe você estiver trabalhando com suas próprias funções, talvez não tenha
medo de exceções se tiver certeza de que seu código nunca produzirá nenhuma.
Ao lidar com dados não avaliados, no entanto, você corre o risco de ver uma exce-
ção lançada durante a avaliação.

Avaliar um único dado lançando uma exceção não é nada especial. Mas avaliar
uma lista de Lazy<A> é mais problemático. O que você deve fazer se um ele-
mento lançar uma exceção durante a avaliação?

Exercício 9.9

Escreva uma sequenceResult funçãocom a seguinte assinatura:

fun <A> sequenceResult(lst: List<Lazy<A>>): Lazy<Result<List<A>>>

Essa função deve retornar um Result of List<A> que não foi avaliado e que se
tornará a Success<List<A>> se todas as avaliações forem bem-sucedidas ou
Failure<List<A>> caso contrário. Aqui está um exemplo de um caso de uso de
teste:

fun main(args: Array<String>) {

val nome1: Preguiçoso<String> = Preguiçoso {


println("Avaliando nome1")
"Mickey"
}

val nome2: Preguiçoso<String> = Preguiçoso {


println("Avaliando nome2")
"Donald"
}

val nome3 = Preguiçoso {


println("Avaliando nome3")
"Pateta"
}

val nome4 = Preguiçoso {


println("Avaliando nome4")
throw IllegalStateException("Exceção ao avaliar name4")
}

val list1 = sequenceResult(List(name1, name2, name3))


val list2 = sequenceResult(List(name1, name2, name3, name4))
val defaultMessage = "Sem saudações quando o tempo é ímpar"
val condição = Random(System.currentTimeMillis()).nextInt() % 2 == 0
println(if (condição) list1() else defaultMessage)
println(if (condição) list1() else defaultMessage)
println(if (condição) list2() else defaultMessage)
println(if (condição) list2() else defaultMessage)
}

Quando a condição for satisfeita, a saída do programa deve ser

Avaliando nome1
Avaliando nome2
Avaliando nome3
Sucesso([Mickey, Donald, Pateta, NIL])
Sucesso([Mickey, Donald, Pateta, NIL])
Avaliando nome4
Falha (exceção ao avaliar name4)
Falha (exceção ao avaliar name4)

E quando a condição não for válida, você deve obter

Sem saudações quando o tempo é estranho


Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho
Sem saudações quando o tempo é estranho

Solução

Como no exercício anterior, você precisa combinar as instruções para produzir o


resultado pretendido. Feito isso, envolva a implementação em um novo Lazy :

import com.fpinkotlin.common.sequence

...

fun <A> sequenceResult(lst: List<Lazy<A>>): Lazy<Result<List<A>>> =


Preguiçoso { sequence(lst.map { Result.of(it) }) }

Você também precisa importar explicitamente a sequence função. Isso faz a dis-
tinção com a sequence função definida no nível do pacote. Outra possibilidade é
usar a traverse funçãovocê criou no capítulo 8:

fun <A> sequenceResult2(lst: List<Lazy<A>>): Lazy<Result<List<A>>> =


Preguiçoso { traverse(lst) { Result.of(it) } }
Avaliar o resultado faz com que todos os elementos da lista sejam avaliados,
mesmo que uma avaliação resulte em um arquivo Failure . A razão é que a
traverse funçãousa a foldRight função. Para fazer a avaliação parar assim
que um elemento for avaliado como um Failure , você teria que usar uma dobra
de escape. A implementação baseada em traverse é equivalente a

fun <A> sequenceResult(lst: List<Lazy<A>>): Lazy<Result<List<A>>> =


Preguiçoso {
lst.foldRight(Result(List())) { x: Lazy<A> ->
{ y: Resultado<Lista<A>> ->
map2(Result.of(x), y) { a: A ->
{ b: Lista<A> ->
b.contras(a)
}
}
}
}
}

Você não tem um escape foldRight , mas desenvolveu um escape foldLeft no


capítulo 8. Você pode usá-lo aqui:

fun <A> sequenceResult(list: List<Lazy<A>>): Lazy<Result<List<A>>> =


Preguiçoso {
val p = { r: Result<List<A>> -> r.map{false}.getOrElse(true) }
list.foldLeft(Result(List()), p) { y: Result<List<A>> ->
{ x: Preguiçoso<A> ->
map2(Result.of(x), y) { a: A ->
{ b: Lista<A> ->
b.contras(a)
}
}
}
}
}

Você pode definir um escape traverse na classe de lista para abstrairistopro-


cesso.

Mais composições preguiçosas

Como você pode ver, compor funções preguiçosamente é uma questão de escre-
ver a composição normal e envolvê-la em uma Lazy instância. Com essa técnica,
você pode compor o que quiser de forma preguiçosa. Não há mágica aqui. Você
sempre pode fazer o mesmo envolvendo qualquer implementação em uma fun-
ção constante:

fun <A> preguiçosoComposição(): Preguiçoso<A> =


Preguiçoso { <qualquer coisa produzindo um A> }

Aqui você está simplesmente escrevendo um programa que é executado quando e


se a Lazy instância for invocada.

9.11.1 Aplicação preguiçosa de efeitos

Sevocê usou os programas de teste fornecidos para os exercícios anteriores, você


já sabe como aplicar efeitos a valores preguiçosos. Você pode obter o valor invo-
cando Lazy e aplicando o efeito ao resultado, como

val preguiçosoString: Preguiçoso<String> = ...


...
println(lazyString())

Mas, em vez de obter o valor de the Lazy e aplicar o efeito, você pode fazer o con-
trário e passar o efeito para o Lazy para que seja aplicado ao valor:

fun forEach(ef: (A) -> Unidade) = ef(valor)

Mas isso não é muito útil se você quiser aplicar condicionalmente um efeito como
o que você fez nos testes anteriores:

if (condição) list1.forEach { println(it) } else println(defaultMessage)

O que você precisa fazer é passar à condição um efeito a ser aplicado se a condi-
ção for válida e outro efeito a ser aplicado caso contrário.

Exercício 9.10

Escreva uma forEach função(um método) na Lazy classe, tomando como parâ-
metros uma condição e dois efeitos e aplicando o primeiro efeito ao valor contido
se a condição for true e o segundo efeito caso contrário.

Dica

Para tornar esta função útil, ela deve permitir a avaliação Lazy somente se neces-
sário. Consequentemente, você precisará definir três versões sobrecarregadas da
função.
Solução

Você pode ficar tentado a escrever algo como

fun forEach(condição: Booleana, ifTrue: (A) -> Unidade, ifFalse: (A) -> Unidade) =
if (condição) ifTrue(valor) else ifFalse(valor)

Mas isso não vai funcionar porque o Kotlin avalia os argumentos das funções if-
True e ifFalse assim que eles são recebidos pela forEach função. Não faz dife-
rença se ambos ifTrue e ifFalse usam o valor. Mas muitas vezes, a ifFal-
se condição não o usa, como no seguinte:

list1.forEach(condição, ::println, { println(defaultMessage)} )

Como o ifFalse manipulador não usa o valor, ele não deve ser avaliado. Mas
também pode ser o contrário, o que significa que o valor será usado se a condição
não for válida. Claro, você pode reverter (negar) a condição. Mas você também
deve cuidar do caso em que ambos os manipuladores precisariam avaliar o ar-
quivo Lazy . Uma possível solução é então escrever três versões da função

fun forEach (condição: Boolean,


ifTrue: (A) -> Unidade,
ifFalse: () -> Unidade = {}) =
se (condição)
iftrue(valor)
outro
ifFalse()

fun forEach (condição: Boolean,


ifTrue: () -> Unidade = {},
ifFalse: (A) -> Unidade) =
se (condição)
se for verdade()
outro
ifFalse(valor)

fun forEach (condição: Boolean,


ifTrue: (A) -> Unidade,
ifFalse: (A) -> Unidade) =
se (condição)
iftrue(valor)
outro
ifFalse(valor)

Esteja ciente, no entanto, de dois problemas. Para permitir que o Kotlin selecione
a função certa, você terá que especificar explicitamente o tipo de cada manipula-
dor, como neste exemplo:

val printMessage: (Qualquer) -> Unidade = ::println


val printDefault: () -> Unidade = { println(defaultMessage)}
list1.forEach(condição, printMessage, printDefault)

A razão para isso é que um manipulador como {} é válido para argumentos de


ambos os tipos (A) -> Unit e () -> Unit .

Se você quiser usar o valor padrão para o ifTrue: () -> Unit argumento (ou
seja, não fazer nada), terá que nomear o argumento nolocal de chamada:

val printMessage: (Qualquer) -> Unidade = ::println


list1.forEach(condição, ifFalse = printMessage)
9.11.2 Coisas que você não pode fazer sem preguiça

Entãoaté agora, pode parecer que a ausência de verdadeira preguiça na avaliação


de expressões em Kotlin não é grande coisa. Afinal, por que você deveria se preo-
cupar em reescrever funções booleanas quando pode usar operadores booleanos?
Há, porém, outros casos em que a preguiça é realmente útil. Existem até vários al-
goritmos que não podem ser implementados sem recorrer à preguiça. Já falei so-
bre o quão inútil if ... else seria uma versão estrita de. Mas pense no se-
guinte algoritmo:

1. Pegue a lista de inteiros positivos.


2. Filtre os primos.
3. Retorna a lista dos dez primeiros resultados.

Este é um algoritmo para encontrar os primeiros dez primos, mas este algoritmo
não pode ser implementado sem preguiça. Se você não acredita em mim, experi-
mente. Comece com a primeira linha. Se você for rigoroso, primeiro avaliará a
lista de números inteiros positivos. Você nunca terá a oportunidade de ir para a
segunda linha porque a lista de inteiros é infinita e você esgotará a memória dis-
ponível antes de chegar ao final (inexistente).

Claramente, este algoritmo não pode ser implementado sem preguiça, mas você
sabe como substituí-lo por um algoritmo diferente. O algoritmo anterior era funci-
onal. Se quiser encontrar o resultado sem recorrer à preguiça, terá que substituí-
lo por um algoritmo imperativo, como este:

1. Pegue o primeiro inteiro.


2. Verifique se é primo.
3. Se estiver, armazene-o em uma lista.
4. Verifique se a lista resultante tem dez elementos.
5. Se tiver dez elementos, retorne-o como resultado.
6. Caso contrário, incremente o inteiro em 1.
7. Vá para a linha 2.

Claro, funciona. Mas que confusão! Primeiro, é uma receita ruim. Você não deve-
ria incrementar o inteiro testado em 2 em vez de 1 para não testar números pa-
res? E por que testar múltiplos de 3, 5 e assim por diante? Mais importante, po-
rém, não expressa a natureza do problema. É apenas uma receita para calcular o
resultado.

Isso não quer dizer que os detalhes da implementação (como não testar números
pares) não sejam importantes para obter um bom desempenho. Mas esses deta-
lhes de implementação devem ser claramente separados da definição do pro-
blema. A descrição imperativa não é uma descrição do problema. É uma descrição
de outro problema dando o mesmo resultado. Uma solução muito mais elegante é
resolver esse tipo de problema com uma estrutura especial: o preguiçosoLista.

9.11.3 Criando uma estrutura de dados de lista preguiçosa

Agoraque você sabe como representar dados não avaliados como instâncias de
Lazy , você pode facilmente definir uma estrutura de dados de lista preguiçosa.
Você a chamará de Stream , e ela será semelhante à lista encadeada isolada-
mente desenvolvida no capítulo 5, com algumas diferenças sutis, mas importan-
tes. A listagem a seguir mostra o ponto de partida do seu Stream tipo de dados.

Listagem 9.3 O Stream tipo de dados

import com.fpinkotlin.common.Result ①
classe selada Stream<out A> { ②

diversão abstrata isEmpty(): Booleano

abstract fun head(): Resultado<A> ③

abstract fun tail(): Resultado<Stream<A>> ④

objeto privado Vazio: Stream<Nothing>() { ⑤

override fun head(): Resultado<Nada> = Resultado()

override fun tail(): Resultado<Nada> = Resultado()

substituir fun isEmpty(): Boolean = true

privado
class Cons<out A> (val interna hd: Lazy<A>, ⑦
val interna tl: Lazy<Stream<A>>):
Stream<A>() { ⑧

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

override fun tail(): Result<Stream<A>> = Result(tl())

substituir fun isEmpty(): Boolean = false


}

objeto complementar {

divertido <A> contras(hd: Preguiçoso<A>,


tl: Lazy<Stream<A>>): Stream<A> =
Contras(hd, tl) ⑨

operador divertido <A> invoca(): Stream<A> =


Vazio ⑩

fun from(i: Int): Stream<Int> =


cons(Preguiçoso { i }, Preguiçoso { from(i + 1) }) ⑪
}
}

① A classe Lazy não é importada aqui porque está no mesmo pacote.

② A classe Stream é selada para impedir a instanciação direta.

③ A função head retorna um Result<A> para que possa retornar Empty quando cha-
mada em um fluxo vazio.

④ Pelo mesmo motivo, a função tail retorna um Result<Stream<A>>.

⑤ A subclasse Empty é exatamente igual à subclasse List.Nil.

⑥ Um stream não vazio é representado pela subclasse Cons.

⑦ A cabeça (chamada hd) não é avaliada, assumindo a forma de um Lazy<A>.

⑧ Da mesma forma, a cauda (tl) é representada por um Lazy<Stream<A>>.

⑨ A função cons constrói um Stream chamando o construtor privado Cons.

⑩ A função do operador de chamada retorna o objeto singleton vazio.


⑪ A função de fábrica retorna um fluxo infinito de números inteiros sucessivos a
partir do valor fornecido.

Aqui está um exemplo de como usar esse Stream tipo:

fun main(args: Array<String>) {


val stream = Stream.from(1)
stream.head().forEach({ println(it) })
stream.tail().flatMap { it.head() }.forEach({ println(it) })
stream.tail().flatMap {
it.tail().flatMap { it.head() }
}.forEach({ println(it) })
}

Este programa imprime o seguinte:

1
2
3

Isso provavelmente não parece útil. Para criar Stream uma ferramenta valiosa,
você precisará adicionar algumas funções a ela. Mas primeiro, você deve otimizá-
lo um pouco.

NOTA É importante entender a diferença entre o Stream descrito neste capítulo,


que é uma lista preguiçosamente avaliada, e o que Java chama Stream , que são
geradores. Geradoressão funções que calculam o próximo elemento de algum es-
tado atual, que pode ser o elemento anterior ou qualquer outra coisa. Em particu-
lar, esse estado externo pode ser uma coleção indexada já avaliada mais um ín-
dice. Este é o caso quando você usa uma construção Java como

Lista<A> lista = ....


lista.stream()...

Neste exemplo Java, o gerador usa o par (lista, índice) como o estado mutável a
partir do qual o novo valor é gerado. Nesse caso, o gerador depende de um estado
mutável externo. Por outro lado, uma vez que o valor é avaliado (por meio de
uma operação de terminal), o fluxo não pode mais ser usado. Kotlin também ofe-
rece geradores como a Sequence construção, embora, ao contrário de Java, não
manipule processamento paralelo.

Isso é diferente do Stream que você está definindo, no qual os dados não são ava-
liados, mas podem se tornar parcialmente avaliados sem impedir a reutilização
do fluxo. No anteriorPor exemplo, você causou a avaliação dos três primeiros va-
lores do stream, mas isso não impede a reutilização do stream, como você pode
ver no exemplo a seguir:

fun main(args: Array<String>) {


val stream = Stream.from(1)
stream.head().forEach({ println(it) })
stream.tail().flatMap { it.head() }.forEach({ println(it) })
stream.tail().flatMap { it.tail()
.flatMap { it.head() } }.forEach({ println(it) })
stream.head().forEach({ println(it) })
stream.tail().flatMap { it.head() }.forEach({ println(it) })
stream.tail().flatMap { it.tail()
.flatMap { it.head() } }.forEach({ println(it) })
}
Este programa exibe

1
2
3
1
2
3

Por outro lado, o seguinte programa Java causaria um IllegalStateExcepti-


on com a mensagem “fluxo já operado ou fechado”:

public class TestStream {


public static void main(String... args) {
Stream<Integer> stream = Stream.iterate(0, i -> i + 1);
stream.findFirst().ifPresent(System.out::println);
stream.findFirst().ifPresent(System.out::println);
}
}

Kotlin oferece a Sequence construção que permite a reutilização, mas não me-
moriza os valores gerados, portanto não é preguiçosolista também.

Manipulando streams

DentroNo restante deste capítulo, você aprenderá como criar e compor fluxos en-
quanto aproveita ao máximo o fato de que os dados não são avaliados. Mas, para
ver os fluxos, você precisará de uma função para avaliá-los. E para avaliar um
stream, você precisa de uma função para limitar seu comprimento. Você não gos-
taria de avaliar um fluxo infinito, não é?
Exercício 9.11

Crie uma função repeat tomando como argumento uma função do tipo () ->
A e retornando um fluxo de A .

Solução

Não há dificuldade aqui. Você precisa construir (cons) uma chamada preguiçosa
para o argumento da função com uma chamada preguiçosa para a repeat pró-
pria função:

fun <A> repeat(f: () -> A): Stream<A> =


cons(Preguiçoso { f() }, Preguiçoso { repetir(f) })

Esta função não causará nenhum problema de pilha porque a chamada para a
repeat função é preguiçosa.

Exercício 9.12

Criar uma takeAtMost funçãopara limitar o comprimento de um fluxo para no


máximo n elementos. Essa função deve funcionar em qualquer fluxo, mesmo que
tenha menos de n elementos. Aqui está sua assinatura:

fun takeAtMost(n: Int): Stream<A>

Dica

Declare uma função abstrata na Stream classee implementá-lo em ambas as sub-


classes usando recursão quando necessário.
Solução

A Empty implementação retorna this :

override fun takeAtMost(n: Int): Stream<Nothing> = this

A Cons implementação testa se o argumento n é maior que 0. Se for, retorna o


head do stream “consed” com o resultado da aplicação recursiva da takeAt-
Most funçãopara a cauda com argumento n - 1 . Se n for menor ou igual a 0, re-
torna o stream vazio:

substituir fun takeAtMost(n: Int): Stream<A> = quando {


n > 0 -> cons(hd, Lazy { tl().takeAtMost(n - 1) })
senão -> Vazio
}

Exercício 9.13

Criar uma dropAtMost funçãopara remover no máximo n elementos do fluxo.


Esta função deve funcionar em qualquer stream, mesmo que tenha menos que n
elementos. Aqui está sua assinatura:

fun dropAtMost(n: Int): Stream<A>

Dica

Declare uma função abstrata na Stream classee implementá-lo em ambas as sub-


classes usando recursão quando necessário.
Solução

A Empty implementação retorna isso:

override fun dropAtMost(n: Int): Stream<Nothing> = this

A Cons implementação testa se o argumento n é maior que 0. Se for, retorna o re-


sultado da chamada recursiva da dropAtMost funçãona cauda do fluxo com o ar-
gumento n - 1 . Se n for menor ou igual a 0, retorna o stream atual:

override fun dropAtMost(n: Int): Stream<A> = quando {


n > 0 -> tl().dropAtMost(n - 1)
senão -> isso
}

Exercício 9.14

Pense nas duas funções takeAtMost e dropAtaMost da perspectiva da recursão.


O que pode acontecer se você chamar essas funções com parâmetros de alto va-
lor? Se você não consegue descobrir a resposta, considere o seguinte exemplo:

fun main(args: Array<String>) {


val stream = Stream.repeat(::random).dropAtMost(60000).takeAtMost(60000)
stream.head().forEach(::println)
}

val rnd = Aleatório()

fun random(): Int {


val rnd = rnd.nextInt()
println("avaliando $rnd")
retorno rnd
}

Este teste causa um StackOverflowException sem avaliar nada! Você pode con-
sertar isso?

Solução

Neste exemplo, takeAtMost não está causando nenhum problema porque está
sendo preguiçoso; nada será avaliado além do valor 60.001. Por outro lado, a
dropAtMost funçãotem que chamar a si mesmo recursivamente 60.000 vezes
para que o fluxo resultante comece com o 60.001º elemento. Essa recursão ocorre
mesmo que nenhum elemento seja avaliado.

A solução é tornar dropAtMost corecursiva, o que geralmente requer o uso de


dois argumentos adicionais: o fluxo no qual a função está operando e um acumu-
lador para o resultado. Mas neste caso particular, você não precisa de um acumu-
lador porque o resultado de cada passo recursivo é ignorado. Apenas o novo
stream (com um elemento removido) e o novo Int parâmetro(diminuído em um)
são necessários. Aqui está a função resultante no objeto complementar:

tailrec fun <A> dropAtMost(n: Int, stream: Stream<A>): Stream<A> = quando {


n > 0 -> quando (fluxo) {
está vazio -> fluxo
é Cons -> dropAtMost(n - 1, stream.tl())
}
senão -> transmitir
}
Como você pode ver, se n atingiu 0 ou se o fluxo está vazio, o argumento do fluxo
é retornado. Caso contrário, a função é chamada recursivamente no final do fluxo
após diminuir o n parâmetro. Para simplificar o uso dessa função, a seguinte fun-
ção de instância é adicionada à Stream classe:

fun dropAtMost(n: Int): Stream<A> = dropAtMost(n, this)

Exercício 9.15

No exercício anterior, eu disse que a takeAtMost funçãonão está criando ne-


nhum problema de pilha porque é preguiçoso, então nada é avaliado. Você pode
se perguntar o que acontece quandoo fluxo é eventualmente avaliado. Para testar
este caso, crie uma toList funçãotransformando um stream em uma lista, avali-
ando todos os elementos. Use esta função para verificar se a takeAtMost função
é inofensiva executando o seguinte programa de teste:

fun main(args: Array<String>) {


val stream = Stream.from(0).dropAtMost(60000).takeAtMost(60000)
println(stream.toList())
}

Dica

Escreva uma função principal na Stream classechamando uma função auxiliar


correcursiva no objeto complementar.
Solução

A função no objeto complementar usará uma função auxiliar correcursiva to-


mando a List<A> como seu parâmetro acumulador. Se o fluxo for Empty , a fun-
ção retornará a lista de acumuladores. Caso contrário, ele chama a si mesmo re-
cursivamente depois de adicionar o início do fluxo à lista e usar o final do fluxo
como seu segundo parâmetro. A função principal chama a função auxiliar com
uma lista vazia como acumulador inicial e inverte a lista resultante antes de re-
torná-la:

fun <A> toList(stream: Stream<A>): List<A> {


tailrec
fun <A> toList(list: List<A>, stream: Stream<A>): List<A> =
quando (fluxo) {
Vazio -> lista
é Cons -> toList(list.cons(stream.hd()), stream.tl())
}
return toList(List(), stream).reverse()
}

Uma função de instância é adicionada à Stream classepara permitir chamá-lo


com notação de objeto:

fun toList(): List<A> = toList(this)

Exercício 9.16

Até agora, o único fluxo que você poderia criar era um fluxo infinito de números
inteiros consecutivos ou um fluxo de elementos aleatórios. Para fazer a Strea-
m aulaum pouco mais útil, crie uma iterate função pegando uma semente do
tipo A e uma função de A para A e retornando um fluxo infinito de A . Em se-
guida, redefina a from função em termos de iterate .

Solução

A solução consiste em usar a cons funçãopara criar um fluxo seed como cabeça
e uma chamada recursiva preguiçosa para a iterate função usando
f(seed) como cauda:

fun <A> iterate(seed: A, f: (A) -> A): Stream<A> =


cons(Preguiçoso { seed }, Preguiçoso { iterar(f(seed), f) })

Embora essa função seja recursiva, ela não explodirá a pilha porque a chamada
recursiva é preguiçosa. Com esta função, você pode redefinir from como

fun from(i: Int): Stream<Int> = iterate(i) { it + 1 }

Um uso interessante dessa função é verificar o que é avaliado. Se você criar uma
função com efeitos colaterais como este

fun inc(i: Int): Int = (i + 1).let {


println("gerando $it")
isto
}

você pode verificar se o seguinte programa de teste avalia apenas valores de 0 a


10.010 antes de imprimir uma lista de 10 valores de 10.000 a 10.009:
fun main(args: Array<String>) {
fun inc(i: Int): Int = (i + 1).let {
println("gerando $it")
isto
}
lista val = Fluxo
.iterate(0, ::inc)
.takeAtMost(60000)
.dropAtMost(10000)
.takeAtMost(10)
.listar()
println(lista)
}

Você também pode criar uma iterate função usando a Lazy<A> como semente
se precisar começar com uma semente não avaliada:

fun <A> iterate(seed: Lazy<A>, f: (A) -> A): Stream<A> =


cons(seed, Lazy { iterate(f(seed()), f) })

Exercício 9.17

Escreva uma takeWhile funçãoque retorna um Stream contendo todos os ele-


mentos iniciais desde que uma condição seja atendida. Aqui está a assinatura da
função na Stream classe pai:

abstract fun takeWhile(p: (A) -> Boolean): Stream<A>


Dica

Esteja ciente de que ao contrário de takeAtMost e dropAtMost , esta função


avalia um elemento porque terá que testar o primeiro elemento para verificar se
cumpre a condição expressa pelo predicado. Você deve verificar se apenas o pri-
meiro elemento do fluxo é avaliado.

Solução

Esta função é semelhante à takeAtMost função. A principal diferença é que a


condição terminal não é mais, n ≤ 0 mas a função fornecida retornando false :

override fun takeWhile(p: (A) -> Boolean): Stream<A> = quando {


p(hd()) -> cons(hd, Preguiçoso { tl().takeWhile(p) })
senão -> Vazio
}

Mais uma vez, você não precisa tornar a função stack-safe porque a chamada re-
cursiva não é avaliada. A Empty implementação retorna this .

Exercício 9.18

Escreva uma dropWhile funçãoque retorna um fluxo com os elementos frontais


removidos, desde que satisfaçam uma condição. Aqui está a assinatura na Stre-
am classe pai:

fun dropWhile(p: (A) -> Boolean): Stream<A>


Dica

Você precisará escrever uma versão corecursiva dessa função para torná-la se-
gura em pilha.

Solução

Como nas funções recursivas anteriores, a solução incluirá uma função na Stre-
am classechamando uma função auxiliar correcursiva segura de pilha no objeto
complementar. Aqui está a função corecursiva no objeto complementar:

tailrec fun <A> dropWhile(stream: Stream<A>,


p: (A) -> Booleano): Stream<A> =
quando (fluxo) {
está vazio -> fluxo
é Contras -> quando {
p(stream.hd()) -> dropWhile(stream.tl(), p)
senão -> transmitir
}
}

E aqui está a função principal na Stream classe pai:

fun dropWhile(p: (A) -> Boolean): Stream<A> = dropWhile(this, p)

Exercício 9.19

No capítulo 8, você criou a seguinte exists funçãoque foi implementado pela


primeira vez na List classeComo
divertido existe(p:(A) -> Booleano): Booleano =
quando isso) {
Nil -> falso
é Cons -> p(cabeça) || cauda.existe(p)
}

Essa função percorria a lista até encontrar um elemento que satisfizesse o predi-
cado p . O resto da lista não foi examinado porque o || operadoré preguiçoso e
não avalia seu segundo argumento se o primeiro for avaliado como true .

Criar uma exists funçãopara Stream . A função deve fazer com que os elemen-
tos sejam avaliados apenas até que a condição seja atendida. Se a condição nunca
for atendida, todos os elementos serão avaliados.

Solução

Uma solução simples poderia ser semelhante à exists funçãoem List :

fun existe(p: (A) -> Boolean): Boolean = p(hd()) || tl().exists(p)

Você deve torná-lo seguro para pilha. Para escrever uma implementação segura
em pilha, você deve torná-la recursiva. Aqui está uma implementação possível no
objeto complementar:

tailrec fun <A> existe(stream: Stream<A>, p: (A) -> Boolean): Boolean =


quando (fluxo) {
Vazio -> falso
é Contras -> quando {
p(stream.hd()) -> verdadeiro
senão -> existe(stream.tl(), p)
}
}

E aqui está a função principal da Stream classe:

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

9.12.1 Fluxos de dobramento

DentroNo capítulo 5, você viu como abstrair a recursão em funções de dobra e


aprendeu a dobrar listas para a direita ou para a esquerda. Dobrar fluxos é um
pouco diferente. Embora o princípio seja o mesmo, a principal diferença é que os
fluxos não são avaliados.

Uma operação recursiva pode estourar a pilha e causar um StackOverflowEx-


ception a ser lançado, mas uma descrição de uma operação recursiva não. A
consequência é que, em muitos casos foldRight , o que não pode ser tornado se-
guro para a pilha em List , não transbordará a pilha em Stream . Ele o transbor-
dará, no entanto, se implicar a avaliação de cada operação, como adicionar os ele-
mentos de um arquivo Stream<Int> . Não o fará se, em vez de avaliar uma ope-
ração, construir uma descrição de uma não avaliada.

Por outro lado, a List implementação de foldRight based on foldLeft (que


pode ser feita com segurança de pilha) não pode ser usada com streams porque
requer a reversão do stream. Isso causaria a avaliação de todos os elementos;
pode até ser impossível no caso de um fluxo infinito. E a versão de pilha segura
foldLeft também não pode ser usada porque inverte a direção da computação.
Exercício 9.20

Criar uma foldRight funçãopara fluxos. Esta função será semelhante à


List.foldRight função, mas você deve cuidar da preguiça.

Dica

A preguiça é expressa pelos elementos sendo Lazy<A> em vez de A . A assinatura


da função na Stream classe pai será

diversão abstrata <B> foldRight(z: Lazy<B>,


f: (A) -> (Preguiçoso<B>) -> B): B

Solução

A implementação na Empty classe é óbvia:

substituir fun <B> foldRight(z: Lazy<B>,


f: (Nada) -> (Preguiçoso<B>) -> B): B = z()

E aqui está a Cons implementação:

substituir fun <B> foldRight(z: Lazy<B>,


f: (A) -> (Preguiçoso<B>) -> B): B =
f(hd())(Preguiçoso { tl().foldRight(z, f) })

Essa função não é segura em pilha, portanto não deve ser usada para cálculos
como a soma de uma lista com mais de mil inteiros. Você verá, no entanto, que ele
tem muitos casos de uso interessantes.
Exercício 9.21

Implemente a takeWhile funçãoem termos de foldRight , chamando-o ta-


keWhileViaFoldRight . Verifique como se comporta em listas longas.

Solução

O valor inicial é a Lazy de um fluxo vazio. A função testa o elemento atual (


p(a) ). Se o resultado for true (o que significa que o elemento atende à condição
expressa pelo predicado p ), um fluxo é retornado “consing” a Lazy { a } para
o fluxo atual:

fun takeWhileViaFoldRight(p: (A) -> Boolean): Stream<A> =


foldRight(Preguiçoso { Vazio }, { a ->
{ b: Preguiçoso<Stream<A>> ->
se (p(a))
cons(Preguiçoso { a }, b)
outro
Vazio
}
})

Como você pode verificar executando os testes fornecidos no código que acompa-
nha este livro ( https://github.com/pysaumont/fpinkotlin ), esta função não trans-
bordará a pilha, mesmo para fluxos com mais de um milhão de elementos. Isso
porque foldRight não avalia o resultado por si só. A avaliação depende da fun-
ção utilizada para fazer a dobra. Se essa função construir um novo fluxo (como no
caso de takeWhile ), esse fluxo não será avaliado.
Exercício 9.22

Implemente headSafe usando foldRight . Essa função deve retornar um


Result.Success do elemento head ou Result.Empty se o fluxo estiver vazio.

Solução

O elemento inicial é um stream vazio não avaliado ( Lazy { Empty } ). Este será
o valor retornado se o stream estiver vazio. A função usada para dobrar o fluxo
ignora o segundo argumento, portanto, na primeira vez em que é aplicada ao
hd elemento, ela retorna Result(a) . Este resultado nunca muda:

divertido headSafeViaFoldRight(): Resultado<A> =


foldRight(Lazy { Result<A>() }, { a -> { Result(a) } })

Exercício 9.23

Implemente map em termos de foldRight . Verifique se esta função não avalia


nenhum dos elementos do fluxo.

Solução

Comece com um Lazy fluxo vazio. A função usada para fazer a dobra será con-
s uma aplicação não avaliada da função ao elemento atual com o resultado atual:

fun <B> map(f: (A) -> B): Stream<B> =


foldRight(Preguiçoso { Vazio }, { a ->
{ b: Preguiçoso<Stream<B>> ->
cons(Preguiçoso { f(a) }, b)
}
})

Exercício 9.24

Implemente filter em termos de foldRight . Verifique se esta função não ava-


lia mais elementos de fluxo do que o necessário.

Solução

Novamente, comece com um fluxo vazio não avaliado. A função usada para do-
brar aplica o filtro ao argumento atual. Se o resultado for true , o elemento é
usado para criar um novo fluxo “consertando-o” com o resultado do fluxo atual.
Caso contrário, o resultado do fluxo atual permanece inalterado (a chamada
b() não avalia nenhum elemento):

fun filter(p: (A) -> Boolean): Stream<A> =


foldRight(Preguiçoso { Vazio }, { a ->
{ b: Preguiçoso<Stream<A>> ->
if (p(a)) cons(Preguiçoso { a }, b) else b()
}
})

Essa função avalia os elementos do fluxo até que a primeira correspondência seja
encontrada. Consulte os testes correspondentes no código acompanhante para ob-
ter detalhes.
Exercício 9.25

Implemente append em termos de foldRight . a append funçãodeve ser pre-


guiçoso em seu argumento.

Dica

Cuidado com a variação!

Solução

Esta função recebe a Lazy<Stream<A>> como argumento, portanto A está na


in posição, embora o parâmetro da Stream classeé declarado out . Como resul-
tado, você deve desabilitar a verificação de variação usando a
@UnsafeVariance anotação.

O elemento inicial é o fluxo (não avaliado) que você deseja anexar. A função fol-
ding cria um novo stream adicionando o elemento atual ao resultado atual
usando a cons função:

fun append(stream2: Lazy<Stream<@UnsafeVariance A>>): Stream<A> =


this.foldRight(stream2) { a: A ->
{ b: Preguiçoso<Stream<A>> ->
Stream.cons(Preguiçoso { a }, b)
}
}

Exercício 9.26

Implemente flatMap em termos de foldRight .


Solução

Novamente, você começa com um fluxo vazio não avaliado. A função é aplicada
ao elemento atual, produzindo um fluxo ao qual o resultado atual é anexado. Isso
tem o efeito de achatar oresultado (transformando a Stream<Stream<B>> em a
Stream<B> ):

fun <B> flatMap(f: (A) -> Stream<B>): Stream<B> =


foldRight(Lazy { Vazio como Stream<B> }, { a ->
{ b: Preguiçoso<Stream<B>> ->
f(a).apêndice(b)
}
})

9.12.2 Avaliação do rastreamento e aplicação da função

Isso éimportante notar a consequência da preguiça. Com coleções estritas como


listas, aplicar a map , a filter e new map sucessivamente implicaria em iterar a
lista três vezes, conforme mostrado aqui:

import com.fpinkotlin.common.List

valor privado f = { x: Int ->


println("Mapeamento " + x)
x * 3
}

valor privado p = { x: Int ->


println("Filtrando " + x)
x % 2 == 0
}
fun main(args: Array<String>) {
val lista = Lista(1, 2, 3, 4, 5).map(f).filter(p)
println(lista)
}

Como você pode ver, funções f e p não são funções puras porque elas registram
no console. Isso ajudará você a entender o que está acontecendo. Este programa
imprime o seguinte:

Mapeamento 1
Mapeamento 2
Mapeamento 3
Mapeamento 4
Mapeamento 5
Filtragem 15
Filtragem 12
Filtragem 9
Filtragem 6
Filtragem 3
[6, 12, NIL]

Isso mostra que todos os elementos são processados ​pela função f , implicando
em uma travessia completa da lista. Em seguida, todos os elementos são processa-
dos ​por função p , implicando uma segunda travessia completa da lista resultante
da primeira map . Por outro lado, observe o seguinte programa, que usa a Strea-
m em vez de a List :

fun main(args: Array<String>) {


val stream = Stream.from(1).takeAtMost(5).map(f).filter(p)
println(stream.toList())
}

Esta é a saída:

Mapeamento 1
Filtragem 3
Mapeamento 2
Filtragem 6
Mapeamento 3
Filtragem 9
Mapeamento 4
Filtragem 12
Mapeamento 5
Filtragem 15
15 [6, 12, NIL]

Você pode ver que a travessia do fluxo ocorre apenas uma vez. Primeiro o ele-
mento 1 é mapeado com f , dando 3 . Em seguida, 3 é filtrado (e descartado por-
que não é um número par). Então 2 é mapeado com f , dando 6 , que é filtrado e
guardado para o resultado.

Como você pode ver, a preguiça dos fluxos permite compor as descrições das com-
putações em vez de seus resultados. A avaliação dos elementos é reduzida ao mí-
nimo. O seguinte resultado é obtido se você usar valores não avaliados para cons-
truir o fluxo e uma função de avaliação com registro ao remover a impressão do
resultado:

gerando 1
Mapeamento 1
Filtragem 3
gerando 2
Mapeamento 2
Filtragem 6

Você pode ver que apenas os dois primeiros elementos são avaliados. As demais
avaliações foram resultado da impressão final.

Exercício 9.27

Escreva uma find função que receba um predicado (uma função de A para Boo-
lean ) como parâmetro e retorne um Result<A> . Isso acontecerá Success se
um elemento corresponder ao predicado ou Empty caso contrário.

Dica

Você não deve ter quase nada para escrever. Combine duas das funções que você
escreveu nas seções anteriores.

Solução

Componha o filter funçãocom head :

fun find(p: (A) -> Boolean): Result<A> = filter(p).head()

9.12.3 Aplicação de fluxos a problemas concretos

DentroNo exercício a seguir, você aplicará fluxos a problemas concretos. Ao fazer


isso, você perceberá como resolver problemas com streams é diferente da aborda-
gem tradicional.
Exercício 9.28

Escreva uma fibs funçãoque gera o fluxo infinito de números de Fibonacci 1, 1,


2, 3, 5, 8 e assim por diante.

Dica

Considere a produção de um fluxo intermediário de par de inteiros usando a


iterate função.

Solução

A solução consiste em criar um fluxo de pares ( x , y ) com x e y sendo dois nú-


meros de Fibonacci sucessivos. Depois que esse fluxo é produzido, você precisa
map dele com uma função de um par para seu primeiro elemento:

fun fibs(): Stream<Int> =


Stream.iterate(Pair(1, 1)) { x ->
Par(x.segundo, x.primeiro + x.segundo)
}.map({ x -> x.first })

Isso pode ser simplificado usando a desestruturação:

fun fibs(): Stream<Int> = Stream.iterate(Pair(1, 1)) {


(x, y) -> Par(y, x + y)
}.map { it.first }

Neste exemplo, (x, y) é inicializado diretamente com o primeiro e o segundo


elemento do Pair , transformando o Pair em uma tupla de suas propriedades. É
o inverso de Pair(x, y) , onde dois valores, x e y , são estruturados ema Pair ,
daí o nome desestruturação .

Exercício 9.29

A iterate função pode ser ainda mais generalizada. Escreva uma unfold fun-
çãoque usa como parâmetros um estado inicial do tipo S e uma função de S para
Result<Pair<A, S>> , e retorna um fluxo de A . Retornar a Result torna possí-
vel indicar se o fluxo deve parar ou continuar.

Usar um estado S significa que a fonte de geração de dados não precisa ser do
mesmo tipo dos dados gerados. Para aplicar esta nova função, escreva novas ver-
sões de fibs e from em termos da unfold função. Aqui está a unfold assina-
tura:

fun <A, S> desdobrar(z: S, f: (S) -> Resultado<Par<A, S>>): Fluxo<A>

Solução

Para começar, aplique a f função ao estado inicial z . Isso produz um arquivo


Result<Pair<A, S>> . Em seguida, mapeie esse resultado com uma função de a
Pair<A, S> , produzindo um fluxo “consing” o primeiro membro do par (o A va-
lor) com uma chamada recursiva (não avaliada) para unfold e usando o segundo
membro do par como o estado inicial. O resultado desse mapeamento é
Success<Stream<A>> ou Empty . Então use getOrElse para retornar o fluxo
contido ou um fluxo vazio padrão:

fun <A, S> desdobrar(z: S, f: (S) -> Resultado<Par<A, S>>): Fluxo<A> =


f(z).map { x ->
Stream.cons(Preguiçoso { x.primeiro }, Preguiçoso { desdobrar(x.segundo, f)
}.getOrElse(Stream.Empty)

Ainda mais simples, você pode usar a desestruturação novamente:

fun <A, S> desdobrar(z: S, f: (S) -> Resultado<Par<A, S>>): Fluxo<A> =


f(z).map { (a, s) ->
Stream.cons(Preguiçoso { a }, Preguiçoso { desdobrar(s, f) })
}.getOrElse(Stream.Empty)

A nova versão de from usa a semente inteira como o estado inicial e uma função
de Int a Pair<Int, Int> . Aqui o estado é do mesmo tipo que o valor:

fun from(n: Int): Stream<Int> = desdobrar(n) { x -> Result(Pair(x, x + 1)) }

a fibs funçãofaz uso mais completo da unfold função. O estado é a Pair<Int,


Int> , e a função produz a Pair<Int, Pair<Int, Int>> :

fun fibs(): Stream<Int> =


Stream.unfold(Pair(1, 1)) { x ->
Resultado(Par(x.primeiro, Par(x.segundo, x.primeiro + x.segundo)))
}

Você pode ver como essas implementações de função são compactas e elegantes!

Exercício 9.30

Usar foldRight para implementar várias funções é uma técnica inteligente. In-
felizmente não serve para filter . Se você testar essa função com um predicado
que não corresponda a mais de 1.000 ou 2.000 elementos consecutivos, ela trans-
bordará a pilha. Escreva uma função de pilha segura filter .

Dica

O problema vem de longas sequências de elementos para os quais o predicado re-


torna false . Pense em uma maneira de se livrar desses elementos.

Solução

A solução é remover a longa série de elementos que retornam false usando


o dropWhile função. Para fazer isso, você deve inverter a condição ( !p(x) ) e,
em seguida, testar se o fluxo resultante está vazio. Se o fluxo estiver vazio, re-
torne-o. (Qualquer fluxo vazio servirá porque o fluxo vazio é um singleton.) Se o
fluxo não estiver vazio, crie um novo fluxo “consertando” a cabeça com a cauda
filtrada.

Como a head função retorna um par, você deve usar o primeiro elemento desse
par como o head elemento do fluxo. Em teoria, você deve usar o elemento certo
do par para qualquer outro acesso. Não fazer isso causaria uma nova avaliação
da cabeça. Mas como você não acessa a cabeça uma segunda vez, apenas a cauda, ​
você pode usar stream.getTail() em vez de. Isso permite evitar o uso de uma
variável local para referenciar o resultado de stream.head() :

fun filter(p: (A) -> Boolean): Stream<A> =


dropWhile { x -> !p(x) }.let { stream ->
quando (fluxo) {
está vazio -> fluxo
é Cons -> cons(stream.hd, Lazy { stream.tl().filter(p)})
}
}

Outra possibilidade é usar a head função. Essa função retorna um


Result<A> que pode ser mapeado para produzir o novo fluxo por meio de uma
chamada recursiva. No final, isso produz um Result<Stream<A>> que ficará va-
zio se nenhum elemento satisfizer o predicado. Tudo o que resta a ser feito é cha-
mar getOrElse isso Result , passando um fluxo vazio comoapadrãovalor:

fun filter2(p: (A) -> Boolean): Stream<A> =


dropWhile { x -> !p(x) }.let { stream ->
quando (fluxo) {
está vazio -> fluxo
é Cons -> stream.head().map({ a ->
cons(Preguiçoso { a }, Preguiçoso { stream.tl().filter(p) })
}).getOrElse(Vazio)
}
}

Resumo

Avaliação estrita significa avaliar os valores assim que eles são referenciados.
Avaliação preguiçosa significa avaliar valores somente se e quando forem
necessários.
Algumas linguagens são rígidas e outras preguiçosas. Alguns são preguiçosos
por padrão e opcionalmente rígidos, outros são rígidos por padrão e opcional-
mente preguiçosos.
Kotlin é uma linguagem estrita. É rigoroso em relação aos argumentos da
função.
Embora o Kotlin não seja preguiçoso, você pode usar a by lazy construção
combinada com uma função para implementar a preguiça.
Com preguiça, você pode manipular e compor estruturas de dados infinitas.
Dobras à direita não causam avaliação de fluxo; apenas algumas funções usa-
das para dobrar o fazem.
Usando dobras, você pode compor várias operações de iteração sem resultar
em várias iterações.
Você pode facilmente definir e compor fluxos infinitos.

Você também pode gostar