12 Functional Input - Output - The Joy of Kotlin
12 Functional Input - Output - The Joy of Kotlin
12 Functional Input - Output - The Joy of Kotlin
Nissocapítulo
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.
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:
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?
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.
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.
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:
Esse tipo pode ser instanciado usando Any como o tipo porque é o pai de todos os
tipos:
Mas, na maioria das vezes, efeitos como funções são usados anonimamente, como
no exemplo a seguir:
Aqui a função { it * 2.35 } não tem nome. Mas você pode dar um nome para
poder reutilizá-lo:
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
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:
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!)
print("Inverso de 4: ")
rt1.forEach(showResult, showError) ③
System.out.print("Inverso de 0: ")
rt2.forEach(showResult, showError) ④
}
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:
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:
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:
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.
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.
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.
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.
import com.fpinkotlin.common.Result
importar java.io.BufferedReader
importar java.io.InputStreamReader
objeto complementar {
operador divertido invocar(): ConsoleReader =
ConsoleReader(BufferedReader(
InputStreamReader(System.`in`))) ③
}
}
③ 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.
nameMessage.forEach(::println, onFailure =
{ println(it.message)}) ④
ageMessage.forEach(::println, onFailure = ⑤
{ println("Idade inválida. Por favor, insira um número inteiro")})
}
① Cria o leitor
Exercício 12.2
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)
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
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:
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
objeto complementar {
Exercício 12.3
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:
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.
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.
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
você poderia fazer a sayHello funçãoretornar um programa que, uma vez exe-
cutado, terá o mesmo efeito:
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.
programa()
computação()
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:
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 :
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"
① Essas três linhas não imprimem nada. São como instruções do DSL.
⑤ Executa o programa
instrução1.plus(instrução2).plus(instrução3)()
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.
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:
objeto complementar {
② 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.)
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
objeto Console {
Solução
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.
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:
Exercício 12.7
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> :
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:
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
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, B, C> map2(ioa: IO<A>, iob: IO<B>, f: (A) -> (B) -> C): IO<C>
Solução
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:
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:
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.
val br = BufferedReader(InputStreamReader(System.`in`))
for (i em 0 até n) {
print("Digite seu nome: ")
nome do val = br.readLine()
println(buildMessage(nome))
}
}
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.
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
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.
Observe que este programa explode a pilha após alguns milhares de iterações.
Isso é equivalente ao seguinte:
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:
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.
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>() ⑤
objeto complementar {
③ A função invoke(this), por sua vez, chama a função invokeHelper que se tornou
recursiva.
⑥ 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.
/**
* 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 ).
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”.