Você está na página 1de 50

12Entrada/saída funcional

Nissocapítulo

Aplicando efeitos com segurança dentro de contextos


Combinando efeitos para sucesso e fracasso
Lendo dados com segurança
Usando as IO estruturas de controle do tipo e do tipo imperativo
Combinando IO operações

Até agora você aprendeu a escrever programas seguros que não produziram ne-
nhum resultado utilizável. Você aprendeu como compor funções verdadeiras para
construir funções mais poderosas. O mais interessante é que você aprendeu a
usar operações não funcionais de maneira segura e funcional. Não funcional ope-
rações são operações que produzem efeitos colaterais, como lançar exceções, alte-
rar o mundo externo ou depender do mundo externo para produzir um resultado.
Por exemplo, você aprendeu como pegar uma divisão inteira, que é uma operação
potencialmente insegura, e transformá-la em uma segura usando-a dentro de um
contexto computacional. Aqui estão alguns exemplos de contextos computacio-
nais que você criou nos capítulos anteriores:

A partir do capítulo 7, o Result tipo permite usar uma função que pode pro-
duzir um erro de forma segura e sem erros.
A partir do capítulo 6, o Option tipo também é um contexto computacional
usado para aplicar com segurança funções que às vezes (para alguns argumen-
tos) não produzem dados.
Nos capítulos 5 e 8, a List classevocê estudou é um contexto computacional,
mas ao invés de lidar com erros, ele permite o uso de funções que funcionam
em elementos únicos no contexto de uma coleção de elementos. Também trata
da ausência de dados, representada por uma lista vazia.
O Lazy tipo do capítulo 9 é um contexto computacional para dados que podem
não ser inicializados até que sejam necessários, e o Stream contexto faz o
mesmo para coleções.

Ao estudar esses tipos, produzir um resultado utilizável não era o objetivo. Neste
capítulo, você aprenderá várias técnicas para produzir resultados práticos de seus
programas. Isso inclui exibir um resultado para seus usuários ou passar um resul-
tado para outro programa.

12.1 O que significa efeitos no contexto?

Lembraro que você fez para aplicar uma função ao resultado de uma operação in-
teira. Digamos que você queira escrever uma inverse funçãoque calcula o in-
verso da multiplicação de um valor inteiro:

val inverse: (Int) -> Result<Double> = { x ->


quando {
x != 0 -> Resultado(1.toDouble() / x)
else -> Result.failure("Divisão por 0")
}
}

Esta função pode ser aplicada a um valor inteiro, mas quando composta com ou-
tras funções, o valor será a saída de outra função! Geralmente já estará no con-
texto, muitas vezes o mesmo tipo de contexto. Aqui está um exemplo:
val ri: Resultado<Int> = ...
val rd: Result<Double> = ri.flatMap(inverse)

É importante observar que você não considera o valor ri fora do contexto para
aplicar a função. Funciona ao contrário: você passa a função para dentro do con-
texto (o Result tipo) para que ela seja aplicada dentro dele, produzindo um novo
contexto que possivelmente encapsula o valor resultante. No trecho anterior, você
está passando a função para o ri contexto, produzindo o novo rd contexto.

Isso é legal e seguro. Nenhuma coisa ruim pode acontecer e nenhuma exceção
pode ser lançada. Esta é a beleza da programação com funções puras: você tem
um programa que sempre funciona, quaisquer que sejam os dados que você usa
como entrada. Mas a questão é: como você pode usar esse resultado? Suponha
que você queira exibir o resultado no console - como você pode fazer isso?

12.1.1 Efeitos de manipulação

Puro funções são definidas como funções sem quaisquer efeitos colaterais obser-
váveis. Um efeito é qualquer coisa que pode ser observada fora do programa. O
papel de uma função é retornar um valor, e os efeitos colaterais são qualquer
coisa além do valor retornado que é observávelde fora da função. É chamado de
efeito colateral porque vem além do valor retornado. Em contraste, um efeito é
como um efeito colateral, mas é a função principal (e geralmente única) de um
programa. Tornar seus programas seguros é obtido escrevendo programas com
funções puras(sem efeitos colaterais) e efeitos puros (sem retornar nenhum valor)
de forma funcional.

A questão é: o que significa lidar com efeitos de maneira funcional? A definição


mais próxima que posso dar neste estágio é esta: lidar com efeitos de uma forma
que não interfira com os princípios da programação funcional, sendo o princípio
mais importante a transparência referencial.

Existem várias maneiras de abordar ou atingir esse objetivo, mas alcançá-lo com-
pletamente pode ser complexo. Muitas vezes, simplesmente abordá-lo é sufici-
ente. Cabe a você decidir qual técnica deseja usar. Aplicar efeitos dentro de con-
textos é a maneira mais simples de fazer com que programas funcionais produ-
zam efeitos observáveis.efeitos.

12.1.2 Efeitos de implementação

ComoComo mencionei, um efeito é qualquer coisa observável de fora do pro-


grama. Para ser valioso, o efeito geralmente deve refletir o resultado do pro-
grama. Sendo assim, geralmente você precisará pegar o resultado do programa e
fazer algo observável com ele.

Observe que observável nem sempre significa observável por um operador hu-
mano. Freqüentemente, o resultado é observável por outro programa, que pode
então traduzir esse efeito em algo observável por um operador humano, de ma-
neira síncrona ou assíncrona.

A impressão na tela do computador pode ser vista pelo operador. Isso geralmente
é o que deveria ser. A gravação em um banco de dados, por outro lado, nem sem-
pre pode ser diretamente visível para um usuário. Às vezes, o usuário procurará o
resultado, mas geralmente ele será lido posteriormente por outro programa. No
capítulo 13, você aprenderá como esses efeitos podem ser usados ​por programas
para se comunicar com outros programas.

Como um efeito geralmente é aplicado a um valor, um efeito puro pode ser mode-
lado como um tipo especial de função, sem retornar nenhum valor. Isso é repre-
sentado em Kotlin pelo seguinte tipo:

(T) -> Unidade

Esse tipo pode ser instanciado usando Any como o tipo porque é o pai de todos os
tipos:

val display = { x: Qualquer -> println(x) }

Ou melhor ainda, você pode usar uma referência de função:

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

Mas, na maioria das vezes, efeitos como funções são usados ​anonimamente, como
no exemplo a seguir:

val ri: Resultado<Int> = ...


val rd: Result<Double> = ri.flatMap(inverse)
rd.map { it * 1,35 }

Aqui a função { it * 2.35 } não tem nome. Mas você pode dar um nome para
poder reutilizá-lo:

val ri: Resultado<Int> = ...


val rd: Result<Double> = ri.flatMap(inverse)
função val: (Double) -> Double = { it * 2,35 }
val resultado = rd.map(função)
Para aplicar efeitos, você precisaria de algo equivalente, como

val ri: Resultado<Int> = ...


val rd: Result<Double> = ri.flatMap(inverse)
função val: (Double) -> Double = { it * 2,35 }
val resultado = rd.map(função)
val ef: (Duplo) -> Unidade = ::println
result.map(ef)

Adivinha? Funciona! Isso é possível porque ef é uma função que retorna Unit (o
equivalente a void em Java, ou melhor, o equivalente a Void ). O código anterior
é equivalente a

val ri: Resultado<Int> = ...


val rd: Result<Double> = ri.flatMap(inverse)
função val: (Double) -> Double = { it * 2,35 }
val resultado = rd.map(função)
val ef: (Duplo) -> Unidade = ::println
val x: Resultado<Unidade> = resultado.map(ef)

Para aplicar um efeito, você pode usar esta técnica, mapeando o efeito e igno-
rando o resultado. Mas você pode fazer melhor. Como você viu no capítulo 7, você
escreveu uma forEach funçãona Result classetomando um efeito e aplicando-o
ao valor subjacente. Esta função foi implementada na Empty classedo seguinte
modo:

substituir fun forEach(onSuccess: (Nothing) -> Unit,


onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) {
onEmpty()
}

Na Success classe, foi implementado assim:

substituir fun forEach(onSuccess: (A) -> Unidade,


onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) {
onSuccess(valor)
}

E na Failure classe, esta foi a implementação:

substituir fun forEach(onSuccess: (A) -> Unidade,


onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) {
onFailure(exceção)
}

a forEach funçãousa três efeitos como parâmetros: um para Success , um para


Failure e um para Empty . Além disso, a declaração abstrata na Result classe
pai declarou valores padrão para cada efeito:

diversão abstrata forEach(onSuccess: (A) -> Unidade = {},


onFailure: (RuntimeException) -> Unidade = {},
onEmpty: () -> Unidade = {})

Você não pode escrever testes de unidade para esta função. Para verificar se fun-
ciona, você pode executar o programa mostrado na listagem a seguir e observar o
resultado na tela. (Você pode escrever alguns testes alterando algum estado global
ou de parâmetro e, em seguida, afirmar essas alterações, mas isso não é um teste
de unidade!)

Listagem 12.1 Dados de saída

fun main(args: Array<String>) {


val ra = Resultado(4) ①
val rb = Resultado(0) ①
val inverse: (Int) -> Result<Double> = { x ->
quando {
x != 0 -> Resultado(1.toDouble() / x)
else -> Result.failure("Divisão por 0")
}
}
val showResult: (Duplo) -> Unidade = ::println
val showError: (RuntimeException) -> Unidade =
{ println("Erro - ${it.message}")}

val rt1 = ra.flatMap(inverso)


val rt2 = rb.flatMap(inverso)

print("Inverso de 4: ")
rt1.forEach(showResult, showError) ③

System.out.print("Inverso de 0: ")
rt2.forEach(showResult, showError) ④
}

① Simula dados retornados por funções que podem falhar

③ Emite o valor resultante


④ Emite a mensagem de erro

Este programa produz o seguinte resultado:

Inverso de 4: 0,25
Inverso de 0: Erro - Divisão por 0

Exercício 12.1

Escreva uma forEach funçãona List classeque toma um efeito e o aplica a to-
dos os elementos da lista. Sua assinatura é esta:

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

Solução

Você pode implementar esta função na List classe paiou declará-la como uma
função abstrata e implementá-la em ambas as subclasses. Ao contrário de
Result.Empty , não hámotivo para avaliar qualquer efeito quando a lista estiver
vazia (embora você possa fazer isso se for adequado ao seu caso de uso). A imple-
mentação para o Nil será esta:

substituir fun forEach(ef: (Nothing) -> Unit) {}

A implementação recursiva mais simples para a Cons classe seria a seguinte:

substituir fun forEach(ef: (A) -> Unidade) {


ef (cabeça)
tail.forEach(ef)
}

Infelizmente, esta implementação irá explodir a pilha se você tiver mais do que
alguns milhares de elementos. A solução é tornar essa função recursiva. A ma-
neira mais fácil é definir localmente uma função auxiliar recursiva de caudaestá
no forEach função:

substituir fun forEach(ef: (A) -> Unidade) {


tailrec fun forEach(list: List<A>) {
quando (lista) {
Nil -> {}
é Contras -> {
ef(lista.head)
forEach(lista.cauda)
}
}
}
paraCada(este)
}

12.2 Leitura de dados

Entãoaté agora, lidamos apenas com a saída. Freqüentemente, a saída de dados


ocorre no final do programa, uma vez que o resultado é calculado. Isso permite
que a maior parte do programa seja escrita sem efeitos com todos os benefícios do
paradigma funcional. Apenas a parte de saída não é funcional.

Não examinamos como inserir dados em programas. Vamos fazer isso agora. Pos-
teriormente, você verá uma maneira mais funcional de inserir dados. Mas pri-
meiro, você verá como fazer isso de uma maneira limpa (embora imperativa) que
se encaixe perfeitamente com as partes funcionais.

12.2.1 Lendo dados do console

ComoPor exemplo, você lerá os dados do console de uma forma que, embora im-
perativa, permita o teste tornando seus programas determinísticos. Você primeiro
desenvolverá um exemplo que lê números inteiros e strings. A listagem a seguir
mostra a interface que você precisará implementar.

Listagem 12.2 Uma interface para entrada de dados

entrada da interface: pode ser fechada { ①

fun readString(): Resultado<Par<String, Input>> ②

fun readInt(): Result<Pair<Int, Input>> ②

fun readString(mensagem: String): Result<Pair<String, Input>> =


readString() ④

fun readInt(message: String): Result<Pair<Int, Input>> =


readInt() ④
}

① Estende a interface Closeable

② Insere um inteiro e uma string, respectivamente

④ Passa uma mensagem como parâmetro


Na listagem, estender a Closeable interface será útil para fechar automatica-
mente os recursos que precisam ser fechados. Passar uma mensagem como parâ-
metro pode ser útil para avisar o usuário.

Mas aqui as implementações padrão fornecidas ignoram a mensagem. Observe


que essas funções retornam Result<Pair<String, Input>> e não
Result<String> , o que permite encadear chamadas de função de maneira refe-
rencialmente transparente.

Você poderia escrever uma implementação concreta para essa interface, mas pri-
meiro escreverá uma abstrata (porque talvez queira ler dados de alguma outra
fonte, como um arquivo). Você colocará o código comum em uma classe abstrata e
o estenderá para cada tipo de entrada. A listagem a seguir mostra essa
implementação.

Listagem 12.3 A AbstractReader implementação

classe abstrata AbstractReader (


leitor de val privado: BufferedReader): Input { ①

substituir divertido readString():


Result<Pair<String, Input>> = tente { ②
leitor.readLine().let {
quando {
it.isEmpty() -> Resultado()
else -> Result(Pair(it, this))
}
}
} catch (e: Exceção) {
Resultado.falha(e)
}
override fun readInt(): Result<Pair<Int, Input>> = try {
leitor.readLine().let {
quando {
it.isEmpty() -> Resultado()
else -> Result(Pair(it.toInt(), this))
}
}
} catch (e: Exceção) {
Resultado.falha(e)
}

override fun close(): Unit = reader.close() ③


}

① Constrói esta classe com um leitor, permitindo diferentes fontes de entrada

② Lê uma linha do leitor e retorna um Result.Empty se a linha estiver vazia, um


Result.Success se algum dado for obtido ou um Result.Failure se algo der errado

③ Delegados para a função de fechamento do BufferedReader

Agora você precisa implementar a classe concreta para ler no console, conforme
mostrado na listagem a seguir. Essa classe é responsável por fornecer o arquivo
reader . Além disso, você reimplementará as duas funções padrão da interface
para exibir um prompt ao usuário.

Listagem 12.4 A ConsoleReader implementação

import com.fpinkotlin.common.Result
importar java.io.BufferedReader
importar java.io.InputStreamReader

class ConsoleReader(leitor: BufferedReader): AbstractReader(leitor) {

override fun readString(mensagem: String):


Result<Pair<String, Input>> { ①
print("$mensagem")
return readString()
}

substituir fun readInt(mensagem: String):


Result<Pair<Int, Input>> { ①
print("$mensagem")
return readInt()
}

objeto complementar {
operador divertido invocar(): ConsoleReader =
ConsoleReader(BufferedReader(
InputStreamReader(System.`in`))) ③

}
}

① As duas funções padrão são reimplementadas para exibir o prompt do usuário.

③ Você deve usar backticks para referenciar o campo in de uma classe Java System
porque in é uma palavra reservada em Kotlin.

Agora você pode usar sua ConsoleReader classecom o que você aprendeu para
escrever um programa completo, da entrada à saída. A listagem a seguir mostra o
programa.

Listagem 12.5 Um programa completo, da entrada à saída

fun main(args: Array<String>) {

val input = ConsoleReader() ①

val rString = input.readString("Digite seu nome:") ②


.map { t -> t.first }

val nameMessage = rString.map { "Olá, $it!" } ③

nameMessage.forEach(::println, onFailure =
{ println(it.message)}) ④

val rInt = input.readInt("Digite sua idade:").map { t -> t.first }

val ageMessage = rInt.map { "Você parece mais jovem que $it!" }

ageMessage.forEach(::println, onFailure = ⑤
{ println("Idade inválida. Por favor, insira um número inteiro")})
}

① Cria o leitor

② Chama readString com um prompt de usuário e retorna um Result<Tuple<String,


Input>>, que é mapeado para produzir um Result<String>

③ A parte comercial do programa (o que o programa deve fazer do ponto de vista do


usuário). Pode ser funcionalmente puro.
④ Aplica o padrão da seção anterior ao resultado ou a uma mensagem de erro

⑤ Imprime uma mensagem diferente daquela na exceção

Observe que não há como se referir ao valor de entrada no


ageMessage.forEach efeito. Para fazer isso, você precisaria usar um contexto
de validação específico em vez de Result.

Este programa não é impressionante. É o equivalente ao onipresente Hello pro-


grama “ ” que geralmente é o segundo exemplo (logo após “ Hello world ”) na
maioria dos livros de programação. Claro, este é apenas um exemplo. O interes-
sante é como é fácil evoluir isso para algo mais útil.

Exercício 12.2

Escreva um programa que peça repetidamente ao usuário para inserir um ID in-


teiro, um nome e um sobrenome e que, posteriormente, exiba a lista de pessoas
no console. A entrada de dados para assim que o usuário insere um ID em branco
e a lista de dados inseridos é exibida.

Dica

Você precisará de uma classe para armazenar cada linha de dados. Use a Per-
son classe de dados mostrada aqui:

classe de dados Person (val id: Int, val firstName: String, val lastName: String)

Implemente a solução em uma main função no nível do pacote. Use a


Stream.unfold funçãopara produzir um fluxo de pessoas. Você pode achar mais
fácil criar uma função separada para inserir os dados correspondentes a uma
única pessoa e usar uma referência de função como argumento de unfold . Esta
função pode ter a seguinte assinatura:

pessoa divertida(entrada: Entrada): Resultado<Par<Pessoa, Entrada>>

Solução

A solução é simples. Considerando que você tem uma função para inserir os da-
dos de uma única pessoa, você pode criar um fluxo de pessoas e imprimir o resul-
tado da seguinte forma (ignorando qualquer erro neste caso e não cuidando de fe-
char recursos):

import com.fpinkotlin.common.List
import com.fpinkotlin.common.Stream

fun readPersonsFromConsole(): List<Pessoa> =


Stream.unfold(ConsoleReader(), ::pessoa).toList()

fun main(args: Array<String>) {


readPersonsFromConsole().forEach(::println)
}

Tudo que você precisa agora é a person função. Essa função solicita o ID, o nome
e o sobrenome, produzindo três Result instâncias que podem ser combinadas
usando o padrão de compreensão que você aprendeu nos capítulos anteriores:

pessoa divertida(entrada: Entrada): Resultado<Par<Pessoa, Entrada>> =


input.readInt("Digite o ID:").flatMap { id ->
id.second.readString("Digite o primeiro nome:")
.flatMap { firstName ->
firstName.second.readString("Digite o sobrenome:")
.map { últimoNome ->
Pair(Pessoa(id.primeiro,
primeiroNome.primeiro,
lastName.first), lastName.second)
}
}
}

O padrão de compreensão é provavelmente um dos padrões mais importantes na


programação funcional, então você deve dominá-lo. Outras linguagens como
Scala ou Haskell têm açúcar sintático para isso, mas Kotlin não. Isso corresponde,
em pseudo-código, a algo assim:

para {
id em input.readInt("Digite o ID:")
firstName em id.second.readString("Digite o primeiro nome:")
lastName em firstName.second.readString("Digite o sobrenome:")
} return Pair(Pessoa(id.first, firstName.first, lastName.first), lastName.second))

Você não deve perder o açúcar sintático. O flatMap idioma talvez seja mais difí-
cil de dominar no começo, mas mostra o que está acontecendo. Muitos programa-
dores conhecem esse padrão como o seguinte:

a.flatMap { b ->
flatMap { c ->
mapa { d ->
obterAlgo(a, b, c, d)
}
}
}

Eles muitas vezes pensam que é sempre umsérie de flatMap s terminando com
um map . Este não é o caso. Se é map ou flatMap depende apenas do tipo de re-
torno. Muitas vezes acontece que a última função (aqui, getSomething ) retorna
um valor simples. É por isso que o padrão termina com um map . Mas se getSo-
mething fosse para retornar um Result , o padrão seria comosegue:

a.flatMap { b ->
flatMap { c ->
flatMap { d ->
obterAlgo(a, b, c, d)
}
}
}

12.2.2Lendo de um arquivo

oa maneira como você projetou o programa torna simples adaptá-lo à leitura de


arquivos. a FileReader classeé semelhante a ConsoleReader . invoke Mas
uma diferença é que a função de objeto complementardeve lidar com o Excep-
tion que pode ser lançado ao criar o BufferedReader , entãoele retorna um
Result<Input> em vez de um valor simples, conforme mostrado na listagem a
seguir.

Listagem 12.6 A FileReader implementação

classe FileReader construtor privado (leitor de valor privado: BufferedReader):


AbstractReader(leitor), AutoCloseable {
substituir divertido fechar () {
leitor.fechar()
}

objeto complementar {

operador fun invoke(path: String): Result<Input> = try {


Result(FileReader(File(path).bufferedReader()))
} catch (e: Exceção) {
Resultado.falha(e)
}
}
}

Exercício 12.3

Escreva um ReadFile programa semelhante a, ReadConsole mas que leia um


arquivo contendo as entradas, cada uma em uma linha separada. Um arquivo de
exemplo é fornecido com o código que acompanha este livro (
http://github.com/pysaumont/fpinkotlin ).

Dica

Embora seja semelhante ao ReadConsole programa, você terá que lidar com o
fato de que a invoke funçãoretorna um Result . Tente reutilizar a mesma per-
son função. Observe também que você deve cuidar de fechar os recursos. Para
isso, você deve usar a use função da biblioteca padrão Kotlin.
Solução

Na solução a seguir, observe como a use função é empregada para garantir que o
arquivo seja fechado corretamente em qualquer caso:

fun readPersonsFromFile(path: String): Result<List<Person>> =


FileReader(caminho).map {
it.use {
Stream.unfold(it, ::person).toList()
}
}

fun main(args: Array<String>) {


val caminho = "<caminho>/data.txt"
readPersonsFromFile(path).forEach({ list: List<Pessoa> ->
list.forEach(::println)
}, onFailure = ::println)
}

A it referência é usada duas vezes na readPersonsFromFile função, cada vez


representando o parâmetro da expressão lambda atual. Se achar isso confuso,
você pode usar nomes específicos como

fun readPersonsFromFile(path: String): Result<List<Person>> =


FileReader(caminho).map { input1 ->
entrada1.use { entrada2 ->
Stream.unfold(input2, ::person).toList()
}
}

Mas neste caso específico, ambos os nomes representam amesmoobjeto.


12.3 Testando com entrada

UmUm dos benefícios da abordagem mostrada é que o programa é facilmente tes-


tável. Seria possível testar seus programas fornecendo arquivos em vez de en-
trada do usuário no console, mas é igualmente fácil fazer a interface de seu pro-
grama com outro programa que produza um script dos comandos de entrada. A
listagem a seguir mostra um exemplo ScriptReader que você pode usar para
testar.

Listagem 12.7 A ScriptReader que permite usar uma lista de comandos de


entrada

class ScriptReader : Entrada {

constructor(commands: List<String>) : super() { ①


this.commands = comandos
}

constructor(vararg comandos: String): super() { ②


this.commands = Lista(*comandos)
}

comandos val privados: List<String>

substituir divertido fechar () {


}

override fun readString(): Result<Pair<String, Input>> = quando {


comandos.isEmpty() ->
Result.failure("Não há entradas suficientes no script")
else -> Result(Pair(commands.headSafe().getOrElse(""),
ScriptReader(commands.drop(1))))
}

override fun readInt(): Result<Pair<Int, Input>> = try {


quando {
comandos.isEmpty() ->
Result.failure("Não há entradas suficientes no script")
Integer.parseInt(commands.headSafe().getOrElse("")) >= 0 ->
Resultado(Par(Inteiro.parseInt(
comandos.headSafe().getOrElse("")),
ScriptReader(commands.drop(1))))
senão -> Resultado()
}
} catch (e: Exceção) {
Resultado.falha(e)
}
}

① O ScriptReader pode ser criado com uma lista de comandos...

② …ou com um argumento vararg.

A listagem a seguir mostra um exemplo de uso da ScriptReader classe. No có-


digo que acompanha este livro, você encontrará exemplos de unidadesteste.

Listagem 12.8 Usando o ScriptReader para inserir dados

fun readPersonsFromScript(vararg comandos: String): List<Pessoa> =


Stream.unfold(ScriptReader(*comandos), ::pessoa).toList()

fun main(args: Array<String>) {


readPersonsFromScript("1", "Mickey", "Mouse",
"2", "Minnie", "Rato",
"3", "Donald", "Pato").forEach(::println)
}

12.4 Entrada/saída totalmente funcional

o quevocê aprendeu até agora é suficiente para a maioria dos programadores Ko-
tlin. Separar a parte funcional do programa das partes não funcionais é essencial
e também suficiente. Mas é interessante ver como os programas Kotlin podem se
tornar ainda mais funcionais.

Você decide se deseja usar as técnicas a seguir em programas Kotlin em produção.


Pode não valer a pena a complexidade adicional, mas é útil aprender essas técni-
cas para que você possa fazer uma escolha informada.

12.4.1 Tornando a entrada/saída totalmente funcional

LáExistem várias respostas para a questão de como você pode tornar a


entrada/saída (E/S) totalmente funcional. A resposta mais curta é que você não
pode. De acordo com a definição de um programa funcional, que é um programa
que não tem outro efeito observável além de retornar um valor, não há como fa-
zer qualquer entrada ou saída.

Mas muitos programas não precisam fazer nenhuma entrada ou saída. Algumas
bibliotecas se enquadram nessa categoria. Bibliotecas são programas projetados
para serem usados ​por outros programas. Eles recebem valores de argumento e
retornam valores resultantes de cálculos baseados nos argumentos. Nas duas pri-
meiras seções deste capítulo, você separou seus programas em três partes: uma
fazendo a entrada, uma fazendo a saída e uma, a terceira parte, atuando como
uma biblioteca e sendo totalmente funcional.
Outra maneira de lidar com o problema é escrever essa parte da biblioteca e pro-
duzir como valor de retorno final outro programa (não funcional) que manipule
todas as entradas e saídas. Isso é semelhante em conceito à preguiça. Você pode
manipular I/O como algo que acontece mais tarde em um programa separado será
o valor retornado de nosso puramente funcionalprograma.

12.4.2Implementando E/S puramente funcional

DentroNesta seção, você verá como implementar E/S puramente funcional. Vamos
começar com a saída. Imagine que você deseja exibir uma mensagem de boas-vin-
das no console. Em vez de escrever isso

fun digaOlá(nome: String) = println("Olá, $nome!")

você poderia fazer a sayHello funçãoretornar um programa que, uma vez exe-
cutado, terá o mesmo efeito:

fun digaOlá(nome: String): () -> Unidade = { println("Olá, $nome!") }

Você pode usar esta função da seguinte maneira:

fun main(args: Array<String>) {


val programa = digaOlá("Georges")
}

Este código é puramente funcional. Você poderia argumentar que não faz nada vi-
sível, e isso é verdade. Ele produz um programa que pode ser executado para pro-
duzir o efeito desejado. Este programa pode ser executado avaliando seu resul-
tado. Este resultado é um programa que não é funcional, mas não nos importa-
mos. O programa principal é funcional.

Como já disse várias vezes, a melhor maneira de tornar os programas seguros é


separar claramente as partes funcionais dos efeitos. A técnica mostrada aqui, se
não for a mais fácil de implementar, é claramente a solução definitiva quando se
trata de separar funções de efeitos.

Isso é trapaça? Não. Pense em um programa escrito em qualquer linguagem fun-


cional. No final, ele é compilado em um programa executável absolutamente não
funcional e que pode ser executado em seu computador. Você está fazendo exata-
mente a mesma coisa aqui, exceto que o programa que você está produzindo pode
parecer escrito em Kotlin. Na verdade, não é. Está escrito em algum tipo deDSL
(Domain-Specific Language) que seu programa está construindo. Para executar
este programa, você pode escrever

programa()

Neste exemplo, o programa produzido é do tipo () → Unit . Isso funciona, mas


seria interessante fazer mais com esse resultado do que simplesmente avaliá-lo.
Por exemplo, você pode querer combinar vários desses tipos de resultados de vá-
rias maneiras úteis. Para isso, você precisa de algo muito mais poderoso, então
você criará um novo tipo chamado IO . Você começará com uma única invo-
ke função. Nesta fase, não é muito diferente de () → Unit :

classe IO(val privado f: () -> Unidade) {

operador divertido invocar() = f()


}
Suponha que você tenha as três funções a seguir:

fun show(mensagem: String): IO = IO { println(mensagem) }

fun <A> toString(rd: Resultado<A>): String =


rd.map { it.toString() }.getOrElse { rd.toString() }

fun inverse(i: Int): Result<Double> = when (i) {


0 -> Result.failure("Div por 0")
senão -> Resultado(1.0/i)
}

Você pode escrever o seguinte programa puramente funcional:

cálculo de val: IO = show(toString(inverse(3)))

Este programa produz outro programa que pode ser posteriormenteexecutado:

computação()

12.4.3 Combinando E/S

Comsua nova IO interface, você pode potencialmente construir qualquer pro-


grama, mas como uma única unidade. Seria interessante poder combinar esses
programas. A combinação mais simples que você pode usar consiste em agrupar
dois programas em um. Isso é o que você fará no próximo exercício.
Exercício 12.4

Crie uma função na IO classe que permite agrupar duas IO instâncias em uma.
Esta função será chamada plus e terá uma implementação padrão. Aqui está a
assinatura:

diversão do operador plus(io: IO): IO

Solução

A solução é retornar um new IO com uma run implementação que primeiro exe-
cuta o atual IO e depois o argumento IO :

operador divertido plus(io: IO): IO = IO {


f()
io.f()
}

Posteriormente, você precisará de um “ do nothing ” IO para servir como ele-


mento neutro para algumas IO combinações. Você pode criar isso facilmente no
IO objeto complementar da seguinte maneira:

objeto complementar {
val vazio: IO = IO {}
}

Usando essas novas funções, você pode criar programas mais sofisticados combi-
nando IO instâncias:
fun getName() = "Mickey"

val instrução1 = IO { print("Olá, ") } ①


val instrução2 = IO { print(getName()) } ①
val instrução3 = IO { print("!\n") } ①

script val: IO = instrução1


+ instrução2
+ instrução3 ④
script() ⑤

① Essas três linhas não imprimem nada. São como instruções do DSL.

④ Combina as três instruções para criar um programa

⑤ Executa o programa

Se preferir, você pode usar chamadas de função em vez de operadores:

instrução1.plus(instrução2).plus(instrução3)()

Você também pode criar um programa a partir de uma lista de instruções:

val script = Lista(


IO { print("Olá, ") },
IO { print(getName()) },
E/S { print("!\n") }
)
Isso parece um programa imperativo? Na verdade, é. Para poder executá-lo, você
deve primeiro compilá-lo em um único IO. Você pode fazer isso com uma dobra
direita:

programa val: IO = script.foldRight(IO.empty) { io -> { io + it } }

Ou uma dobra à esquerda:

programa val: IO = script.foldLeft(IO.empty) { acc -> { acc + it } }

Você pode ver por que precisava de uma implementação sem fazer nada. Final-
mente, você pode executar o programa normalmente:

programa()

Esteja ciente de que usar uma dobra à esquerda faz com que a identidade IO seja
colocada na primeira posição, ao contrário de uma dobra à direita na qual a iden-
tidade IO é colocada na última posição. Usar IO.empty para a identidade não faz
diferença. Mas se você usar outro IO (por exemplo, alguma tarefa de inicializa-
ção), provavelmente terá que ser executado primeiro, então você terá que usar
uma dobra à esquerda. Use uma dobra à direita se quiser usar alguma tarefa de
finalização como identidade.

12.4.4 Manipulação de entrada com IO

Noneste ponto, seu IO tipo lida apenas com a saída. Para que ele trate a entrada,
uma mudança necessária é parametrizar o mesmo com o tipo do valor de entrada
para que ele possa ser usado para tratar esse valor. Aqui está o novo IO tipo
parametrizado:

class IO<out A>(private val f: () -> A) { ①

operador divertido invocar() = f()

objeto complementar {

val vazio: IO<Unidade> = IO { } ②

operador divertido <A> invocar(a: A): IO<A> = IO { a } ③


}
}

① A classe IO é parametrizada e a função com a qual ela é construída retorna uma


instância do tipo de parâmetro de classe.

② A instância vazia é parametrizada com Unit e criada com uma função que não re-
torna nada. (Esteja ciente de que isso é diferente de retornar Nothing.)

③ A função de invocar do objeto companheiro pega um valor simples e o retorna no


contexto de E/S.

Como você pode ver, a IO interface cria um contexto para cálculos da mesma
forma que Option , Result , List , Stream , Lazy e similares. Da mesma
forma, possui uma função que retorna uma instância vazia, bem como uma fun-
ção que coloca um valor simples no contexto.

Para realizar cálculos em IO valores, agora você precisa de funções como ma-
p e flatMap para vincular funções ao IO contexto. Mas para poder testar essas
funções, você primeiro definirá um objeto para representar o console do
computador.

Exercício 12.5

Defina um Console objeto com as três funções a seguir:

objeto Console {

fun readln(): IO<String> = TODO("")

fun println(o: Qualquer): IO<Unit> = TODO("")

fun print(o: Any): IO<Unit> = TODO("")


}

Solução

as funções print e println chame as funções equivalentes do Kotlin em seus ar-


gumentos assim:

fun println(o: Qualquer): IO<Unidade> = IO {


kotlin.io.println(o.toString())
}

print divertido(o: Qualquer): IO<Unidade> = IO {


kotlin.io.print(o.toString())
}

Você deve usar os nomes de função totalmente qualificados para evitar que as
funções chamem a si mesmas. Você pode, no entanto, chamar suas funções por
qualquer outro nome.

a readln funçãochama a realLine funçãode um BufferedReader embrulho


System.in . Lembre-se de que você deve usar backticks porque in é uma palavra
reservada em Kotlin:

private val br = BufferedReader(InputStreamReader(System.`in`))

fun readln(): IO<String> = IO {


tentar {
br.readLine()
} catch (e: IOException) {
lance IllegalStateException(e)
}
}

Para simplificar, você pode lançar uma exceção se algo der errado. Sinta-se à von-
tade para lidar com o problema de uma maneira mais funcional, se preferir.

O Console objeto, nesse estágio, traz apenas complexidade adicional sem ne-
nhum benefício. Para poder compor IO instâncias, você precisará de uma
map função.

Exercício 12.6

Defina uma map função em IO<A> que receba como argumento uma função de
A to B e retorne um IO<B> .
Solução

Aqui está a implementação que aplica a função ao valor de this e retorna o re-
sultado em um novo IO contexto:

divertido mapa <B> (g: (A) -> B): IO<B> = IO {


g(este())
}

Veja como você pode usar esta função:

fun main(args: Array<String>) {


val script = digaOlá()
roteiro()
}

private fun sayHello(): IO<Unit> = Console.print("Digite seu nome: ")


.map { Console.readln()() }
.map { buildMessage(it) }
.map { Console.println(it)() }

private fun buildMessage(nome: String): String = "Olá, $nome!"

Exercício 12.7

Invocar o IO repetidamente como em Console.readln()() e


Console.println(it)() é complicado. Isso é necessário porque as funções re-
adln e println retornam IO instâncias e não valores brutos. Escreva uma
flatMap funçãopara abstrair esse processo. Esta função recebe uma função de
A para IO<B> como seu argumento e retorna um IO<B> .
Solução

A solução é óbvia. Tudo o que você precisa fazer é invocar o IO<IO<B>> que a
map função retorna para nivelá-la em IO<B> :

fun <B> flatMap (g: (A) -> IO<B>): IO<B> = IO {


g(este())()
}

Como você pode ver, isso é meio recursivo. Não será um problema a princípio
porque há apenas uma etapa de recursão, mas pode se tornar um problema se
você encadear um grande número de flatMap chamadas. Agora você pode com-
por I/O de forma funcional:

fun main(args: Array<String>) {


val script = digaOlá()
roteiro()
}

private fun sayHello(): IO<Unit> = Console.print("Digite seu nome: ")


.flatMap { Console.readln() }
.map { buildMessage(it) }
.flatMap { Console.println(it) }

private fun buildMessage(nome: String): String = "Olá, $nome!"

a sayHello funçãoé totalmente seguro. Ele nunca lança um IOException por-


que não executa nenhuma operação de E/S. Ele retorna apenas um programa que
realiza essas operações uma vez executado, o que é feito invocando o valor retor-
nado ( script() ). Somente esta invocação pode lançar umexceção.
12.4.5 Estendendo o tipo IO

Porusando o IO tipo, você pode criar programas impuros (programas com efei-
tos) de forma puramente funcional. Mas, neste estágio, esses programas só permi-
tem que você leia e imprima em um elemento como sua Console classe. Você
pode estender seu DSL adicionando instruções para criar estruturas de controle,
como loops e condicionais.

Primeiro, você implementará um loop semelhante ao for loop indexado. Isso as-
sumirá a forma de uma repeat funçãoque leva o número de iterações e a IO re-
petir como seus parâmetros.

Exercício 12.8

Implemente a repeat funçãono IO objeto complementar com a seguinte


assinatura:

fun <A> repeat(n: Int, io: IO<A> ): IO<List<A>>

Dica

Para simplificar a codificação na IO classe, você pode precisar de uma fill fun-
ção adicionalno Stream objeto companheiro. Esta função cria um fluxo de n ele-
mentos não avaliados:

fun <A> fill(n: Int, elem: Lazy<A>): Stream<A> {


tailrec
fun <A> fill(acc: Stream<A>, n: Int, elem: Lazy<A>): Stream<A> =
quando {
n <= 0 -> acc
else -> fill(Cons(elem, Lazy { acc }), n - 1, elem)
}
return fill(Empty, n, elem)
}

Você deve criar uma coleção de IO , representando cada iteração e, em seguida,


dobrar essa coleção combinando IO instâncias. Para fazer isso, você precisará de
algo mais poderoso do que a plus função. Comece implementando uma
map2 função com a seguinte assinatura:

fun <A, B, C> map2(ioa: IO<A>, iob: IO<B>, f: (A) -> (B) -> C): IO<C>

Solução

A map2 função pode ser implementada da seguinte forma:

fun <A, B, C> map2(ioa: IO<A>, iob: IO<B>, f: (A) -> (B) -> C): IO<C> =
ioa.flatMap { a ->
iob.map { b ->
f(a)(b)
}
}

Esta é uma aplicação simples do padrão de compreensão ubíqua. Com esta função
em mãos, você pode facilmente implementar repeat da seguinte forma:

fun <A> repeat(n: Int, io: IO<A> ): IO<List<A>> =


Stream.fill(n, Preguiçoso { io })
.foldRight( Lazy { IO { List<A>() } }) { ioa ->
{ siLa ->
map2(ioa, sioLa()) { a ->
{ la: List<A> -> cons(a, la) }
}
}
}

Isso pode parecer um pouco complexo, mas isso ocorre em parte porque a linha
foi quebrada para impressão e em parte porque foi escrita como uma linha única
para otimização. É equivalente a isto:

fun <A> repeat(n: Int, io: IO<A> ): IO<List<A>> {


val stream: Stream<IO<A>> = Stream.fill(n, Lazy { io })
val f: (A) -> (Lista<A>) -> Lista<A> =
{a->
{ la: List<A> -> cons(a, la) }
}
val g: (IO<A>) -> (Lazy<IO<List<A>>>) -> IO<List<A>> =
{ioa->
{ siLa ->
map2(ioa, siLa(), f)
}
}
val z: Lazy<IO<List<A>>> = Lazy { IO { List<A>() } }
return stream.foldRight(z, g)
}

Dica Se você estiver usando um IDE, é relativamente fácil encontrar os tipos. Por
exemplo, no IntelliJ, você precisa colocar o ponteiro do mouse em uma referência
enquanto mantém pressionada a tecla Ctrl para exibir o tipo.

Com essas funções, agora você pode escrever o seguinte:


val program = IO.repeat(3, sayHello())

Isso fornece um programa equivalente a chamar a seguinte função como


sayHello(3) :

divertido dizerOlá(n: Int) {

val br = BufferedReader(InputStreamReader(System.`in`))

for (i em 0 até n) {
print("Digite seu nome: ")
nome do val = br.readLine()
println(buildMessage(nome))
}
}

A diferença importante, no entanto, é que a chamada sayHello(3) executa o


efeito três vezes ansiosamente, enquanto IO.repeat(3, sayHello()) retorna
um programa (não avaliado) que faz o mesmo somente quando sua invoke fun-
çãoé chamado.

É possível definir muitas outras estruturas de controle. Você encontrará exemplos


no código acompanhante que pode ser baixado em
http://github.com/pysaumont/fpinkotlin .

A listagem a seguir mostra um exemplo de uso condition e doWhile funções


que fazem exatamente a mesma coisa if e while na maioria das linguagens
usuais.

Listagem 12.9 Usando IO para agrupar a programação imperativa


private val buildMessage = { nome: String ->
IO.condition(name.isNotEmpty(), Preguiçoso {
IO("Olá, $nome!").flatMap { Console.println(it) }
})
}

fun program(f: (String) -> IO<Booleano>, título: String): IO<Unidade> {


return IO.sequence(Console.println(título),
IO.doWhile(Console.readln(), f),
Console.println("tchau!")
)
}

fun main(args: Array<String>) {


val program = program(buildMessage,
"Digite os nomes das pessoas a serem bem-vindas: ")
programa()
}

Este exemplo não pretende sugerir que você deva programar assim. Certamente é
melhor usar o IO tipo apenas para E/S, fazendo todos os cálculos na programação
funcional. Implementar uma DSL imperativa em código funcional pode não ser a
solução mais eficiente, seja qual for o problema. Mas é importante fazer isso
como um exercício para entender comofunciona.

12.4.6 Tornando o tipo IO seguro para pilha

DentroNos exercícios anteriores, você pode não ter notado que algumas das
IO funções usavam a pilha da mesma forma que as funções recursivas. a repe-
at função, por exemplo, transborda a pilha se o número de repetições for muito
alto. Quanto é muito alto depende do tamanho da pilha e quão cheia ela está
quando o programa retornado pela função é executado. (Até agora, espero que
você entenda que chamar a repeat funçãonão vai explodir a pilha. Somente exe-
cutar o programa que ele retorna pode fazer isso.)

Exercício 12.9

Para experimentar explodir a pilha, crie uma forever funçãoque recebe um


IO como argumento e retorna um novo IO executando o argumento em um loop
infinito. Aqui está a assinatura correspondente no IO objeto complementar:

divertido <A, B> para sempre(ioa: IO<A>): IO<B>

Solução

Isso é tão simples de implementar quanto inútil! Tudo o que você precisa fazer é
tornar o programa construído infinitamente recursivo. Esteja ciente de que a fo-
rever funçãoem si não deve ser recursivo. Somente o programa retornado deve
ser.

A solução é usar uma função auxiliar do tipo () → IO , e para flatMap o IO ar-


gumento da forever funçãocom uma função invocando esta função auxiliar:

divertido <A, B> para sempre(ioa: IO<A>): IO<B> {


val t: () -> IO<B> = { forever(ioa) }
return ioa.flatMap { t() }
}

Esta função pode ser utilizada da seguinte forma:


fun main(args: Array<String>) {
val program = IO.forever<String, String>(IO { "Oi de novo!" })
.flatMap { Console.println(it) }
programa()
}

Observe que este programa explode a pilha após alguns milhares de iterações.
Isso é equivalente ao seguinte:

IO.forever<Unit, String>(Console.println("Oi de novo!"))()

Se você não entender por que isso explode a pilha, considere o seguinte pseudo-
código (que não compila!) em que a t variável na forever funçãoimplementa-
ção é substituída pela expressão correspondente:

divertido <A, B> para sempre(ioa: IO<A>): IO<B> {


return ioa.flatMap { { forever(ioa) }() }
}

Agora vamos substituir a chamada recursiva pelo código correspondente da fo-


rever funçãoimplementação:

divertido <A, B> para sempre(ioa: IO<A>): IO<B> {


return ioa.flatMap { { ioa.flatMap { { forever<A, B>(ioa) }() } }() }
}

Você poderia continuar para sempre recursivamente. O que você pode notar é
que as chamadas para flatMap seriam aninhadas, resultando no envio do estado
atual para a pilha com cada chamada. Isso realmente explodiria a pilha depois de
alguns milhares de etapas. Ao contrário do código imperativo, onde você executa
uma instrução após a outra, você está chamando a flatMap funçãorecursiva-
mente.

Para tornar a IO pilha segura, você pode usar uma técnica chamadatrampolim .
Primeiro, você precisará representar três estados do seu programa:

Return representa uma computação concluída, o que significa que você pre-
cisa retornar o resultado.
Suspend representa uma computação suspensa quando algum efeito deve ser
aplicado antes de retomar a computação atual.
Continue representa um estado onde o programa deve primeiro aplicar uma
sub-computação antes de continuar com a próxima.

Esses estados são representados pelas três classes mostradas na listagem a seguir.

OBSERVAÇÃO AS Listagens 12.9 a 12.11 são partes de um todo. Eles não devem
ser usados ​com o código anterior, mas juntos.

Listagem 12.10 As três classes necessárias para tornar a IO pilha segura

classe selada IO<out A> { ①

interno
class Return<out A>(val value: A): IO<A>() ②

interno
class Suspend<out A>(val resume: () -> A): IO<A>() ③

interno
class Continue<A, out B>(val sub: IO<A>, ④
val f: (A) -> IO<B>): IO<A>() ⑤

① IO agora é uma classe selada para evitar a instanciação de fora da classe.

② Este valor será retornado pelo cálculo.

③ Esta função sem argumento aplica um efeito (colateral) e retorna um valor.

④ Este IO é executado primeiro, produzindo um valor.

⑤ O cálculo continua aplicando esta função ao valor retornado.

Algumas modificações devem ser feitas na IO classe envolvente, conforme mos-


trado nas listagens 12.11 e 12.12.

Listagem 12.11 Mudanças na versão stack-safe de IO

classe selada IO<out A> { ①

operador divertido invocar(): A = invocar(este) ②

operador divertido invocar(io: IO<@UnsafeVariance A>): A { ③


tailrec fun invokeHelper(io: IO<A>): A =
quando (io) { ④
...
}
return invocaHelper(io)
}

divertido <B> mapa(f: (A) -> B): IO<B> =


flatMap { Return(f(it)) } ⑤

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


Continue(this, f) as IO<B> ⑥

class IORef<A>(private var value: A) {

conjunto divertido(a: A): IO<A> {


valor = um
unidade de retorno (a)
}

fun get(): IO<A> = unidade(valor)

fun modify(f: (A) -> A): IO<A> = get().flatMap({ a -> set(f(a)) })


}

classe interna Return<out A>(val value: A): IO<A>()

classe interna Suspend<out A>(val resume: () -> A): IO<A>()

classe interna Continue<A, out B>(val sub: IO<A>,


val f: (A) -> IO<B>): IO<A>()

objeto complementar {

val vazio: IO<Unidade> = IO.Suspend { Unidade } ⑦

diversão interna <A> unidade(a: A): IO<A> =


IO.Suspend { a } ⑧

// resto da classe omitido


}
}

① O tipo IO agora é uma classe selada.

② A função de invocar agora chama a função auxiliar de invocar(este).

③ A função invoke(this), por sua vez, chama a função invokeHelper que se tornou
recursiva.

④ A função invokeHelper é mostrada na Listagem 12.12 .

⑤ A função map agora é definida em termos de aplicação de flatMap à composição


de f e o construtor Return.

⑥ A função flatMap retorna um Continue que é convertido em um IO<B>.

⑦ O IO vazio agora é um Suspenso.

⑧ A função de unidade retorna um Suspender.

Listagem 12.12 invokeHelper A função stack-safe

tailrec fun invokeHelper(io: IO<A>): A = when (io) {


é Return -> io.value ①
é Suspend -> io.resume() ②
senão -> {
val ct = io as Continue<A, A> ③
val sub = ct.sub
val f = ct.f
quando (sub) {
is Return -> invokeHelper(f(sub.value)) ④
is Suspend -> invokeHelper(f(sub.resume())) ⑤
senão -> {
val ct2 = sub as Continue<A, A> ⑥
val sub2 = ct2.sub
val f2 = ct2.f
invocaHelper(sub2.flatMap { f2(it).flatMap(f) })
}
}
}
}

① Se o IO recebido for um Return, o cálculo está concluído.

② Se o IO recebido for um Suspend, o efeito contido é executado antes de retornar o


valor de retomada.

③ Se o IO recebido for um Continue, o sub IO contido será lido.

④ Se sub for um Return, a função é chamada recursivamente com o resultado da


aplicação da função incluída a ela.

⑤ Se sub for um Suspend, a função incluída é aplicada a ele, possivelmente produ-


zindo o efeito correspondente.

⑥ Se sub for um Continue, o IO que ele contém é extraído (sub2) e flatMapped com
sub, o que cria o encadeamento.

A listagem a seguir mostra como você pode usar a nova versão de pilha segura.

Listagem 12.13 A nova versão stack-safe da Console classe


objeto Console {

private val br = BufferedReader(InputStreamReader(System.`in`))

/**
* Uma possível implementação de readLine como uma função val
*/
val readLine2: () -> IO<String> = {
IO. Suspender {
tentar {
br.readLine()
} catch (e: IOException) {
lance IllegalStateException(e)
}
}
}

/**
* Uma implementação mais simples de readLine. Uma referência de função não é
* possível devido ao conflito de nomes. Use nomes diferentes para se divertir e valer
* funções se você quiser usar referências de função
*/
val readLine = { readLine() }

/**
* Uma versão divertida do readLine
*/
fun readLine(): IO<String> = IO.Suspend {
tentar {
br.readLine()
} catch (e: IOException) {
lance IllegalStateException(e)
}
}
/**
* Uma versão válida do printLine
*/
val printLine: (String) -> IO<Unidade> = { s: Qualquer ->
IO. Suspender {
println(s)
}
}

/**
* Uma versão divertida do printLine
*/
fun printLine(s: Any): IO<Unit> = IO.Suspend { println(s) }

/**
* Uma função de impressão. Observe a chamada totalmente qualificada para kotlin.io.pri
*/
print(s: Any): IO<Unit> = IO.Suspend { kotlin.io.print(s) }
}

Agora você pode usar forever ou doWhile sem o risco de transbordar a pilha.
Você também pode reescrever repeat para torná-lo seguro para pilha. Não mos-
trarei a nova implementação aqui, mas você a encontrará no código que a acom-
panha ( http://github.com/pysaumont/fpinkotlin ).

Tenha em mente que esta não é a maneira recomendada de escrever programas


funcionais. Tome-o como um exemplo do que pode ser feito, em vez de uma boa
prática. Observe também que “finalmente”, aqui, se aplica à programação Kotlin.
Com uma linguagem amigável mais funcional, você pode criar muitomaispodero-
soprogramas.
Resumo

Os efeitos podem ser passados ​para List , Result e outros contextos para se-
rem aplicados com segurança aos valores, em vez de extrair valores desses
contextos e aplicar os efeitos externos, o que pode produzir erros se não hou-
ver valores.
A manipulação de dois efeitos diferentes para sucesso e falha pode ser abs-
traída dentro do Result tipo.
A leitura dos arquivos é feita exatamente da mesma forma que a leitura do
console ou da memória por meio da Reader abstração.
Entradas/saídas mais funcionais podem ser obtidas através do IO tipo.
O IO tipo pode ser estendido para um tipo mais genérico que permite realizar
qualquer tarefa imperativa de forma funcional construindo um programa que
é executado posteriormente.
O IO tipo pode ser empilhado com segurança usando uma técnica conhecida
como “trampolim”.

Você também pode gostar