You are on page 1of 65

14Resolvendo problemas comuns

funcionalmente

Este capítulocapas

Usando asserções
Novas tentativas automáticas para funções com falha ou aplicativos de efeito
Lendo arquivos de propriedades
Adaptando bibliotecas imperativas
Convertendo programas imperativos
Efeitos repetitivos

Agora você tem muitas ferramentas que podem facilitar sua vida como programa-
dor usando técnicas de programação seguras vindas do mundo da programação
funcional. Mas conhecer as ferramentas não é suficiente. Para se tornar eficiente
usando técnicas funcionais, você deve torná-las uma segunda natureza. Você pre-
cisa pensar funcionalmente. Assim como os programadores orientados a objetos
(OO) pensam em padrões, os programadores funcionais fazem o mesmo com as
funções.

Quando os programadores OO têm um problema para resolver, eles procuram pa-


drões de projeto que possam reconhecer e tentam reduzir o problema a uma com-
posição de padrões. Depois de fazer isso, eles precisam implementar os padrões e
compô-los.
Os programadores funcionais fazem o mesmo com funções com uma diferença:
quando descobrem que uma função pode ser usada para resolver um problema,
eles não precisam reimplementá-la. Eles podem reutilizá-lo, porque as funções, ao
contrário dos padrões de design, são códigos reutilizáveis.

Os programadores funcionais tentam reduzir cada problema a uma composição


de funções previamente implementadas. Isso nem sempre é possível, então às ve-
zes eles precisam implementar novas funções. Mas essas novas funções se tornam
parte de sua caixa de ferramentas. A principal diferença aqui é que os programa-
dores funcionais estão sempre buscando a abstração porque a abstração é o que
torna as funções reutilizáveis.

Todos os programadores usam bibliotecas que oferecem mais ou menos a mesma


funcionalidade: generalizar problemas de forma a permitir a reutilização de có-
digo em vez de reinventar a roda cada vez que um novo problema precisa ser re-
solvido. A diferença está no nível de abstração. A abstração prematura é conside-
rada um pecado na programação OO, enquanto é uma das ferramentas funda-
mentais da programação funcional. A abstração permite ao programador não
apenas reutilizar funções, mas também entender melhor a verdadeira natureza
dos problemas em questão.

Neste capítulo, você verá alguns problemas comuns que os programadores preci-
sam resolver na vida profissional diária. Você verá como pode abordar esses pro-
blemas de maneira diferente usando o paradigma funcional. Além de aprender a
resolver esses problemas cotidianos de maneira funcional, muitas vezes você pre-
cisará usar código imperativo. Mas qual é a melhor abordagem para usar esse có-
digo com programas funcionais? Começaremos com um programa imperativo e o
modificaremos para torná-lo mais eficiente e útil.
14.1 Afirmações e validação de dados

Asserçõessão usados ​para verificar invariantes como pré-condições, pós-condi-


ções, condições de fluxo de controle e condições de classe. Na programação funci-
onal, geralmente não há fluxo de controle e as classes geralmente são imutáveis,
portanto, as únicas condições a serem verificadas são pré e pós-condições. Estes
pelos mesmos motivos (imutabilidade e ausência de fluxo de controle) consistem
em testar os argumentos recebidos pelos métodos e funções, e testar seus resulta-
dos antes de devolvê-los. Testar o valor do argumento é necessário em funções
parciais como esta:

divertido inverso(x: Int): Duplo = 1,0 / x

Esta função retorna um valor utilizável para qualquer entrada, exceto para 0,
para o qual retorna infinito. Como você provavelmente não pode fazer nada com
esse valor, talvez prefira tratá-lo de uma maneira específica. Na programação im-
perativa, você poderia escrever isto:

divertido inverso(x: Int): Duplo {


assert(x == 0)
retorno 1,0/x
}

Este snippet usa asserções Java padrão, que estão disponíveis em Kotlin. Mas
como as asserções podem ser desativadas em tempo de execução, você pode que-
rer impedir que o programa seja executado com asserções desativadas usando o
seguinte:
if (!Thread.currentThread().javaClass.desiredAssertionStatus()) {
throw RuntimeException("Asserts devem ser habilitados!!!")
}

OBSERVAÇÃO Se você estiver executando seu código em algumas versões


mais antigas do IntelliJ, as asserções podem estar habilitadas por padrão. Nesses
casos, você deve desabilitar explicitamente as asserções usando o -da parâmetro
VMem sua configuração para simular a execução normal. Neste caso específico, é
mais simples escrever:

fun inverse(x: Int): Double = when (x) {


0 -> throw IllegalArgumentException("div. Por 0")
senão -> 1,0 / x
}

Para ser seguro, a função deve ser transformada em uma função total da seguinte
forma:

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


0 -> Result.failure("div. Por 0")
senão -> Resultado(1.0 / x)
}

A forma mais genérica de asserção consiste em testar um argumento contra uma


condição específica, retornando um Result.Failure se a condição não for cor-
respondida e um Result.Success caso contrário. Veja o exemplo de uma invo-
ke função de operador para um Person tipo:
class Person private constructor(val id: Int,
val primeiroNome: String,
val últimoNome: String) {

objeto complementar {
operador
divertido invocar(id: Int?,
primeiroNome: String?,
últimoNome: String?): Pessoa =
Pessoa(id, primeiroNome, sobrenome)
}
}

Esta função pode ser usada com dados extraídos de um banco de dados:

val pessoa = Pessoa(rs.getInt("pessoaId"),


rs.getString("primeiroNome"),
rs.getString("últimoNome"))

Nesse caso, convém validar os dados antes de chamar a função. Por exemplo, você
pode querer verificar se o ID é positivo e se o nome e o sobrenome não são nul-
l ou estão vazios e começam com uma letra maiúscula. Na programação impera-
tiva, você pode verificar isso testando cada condição antes de chamar a função
por meio do uso de funções de asserção:

class Person private constructor(val id: Int,


val primeiroNome: String,
val últimoNome: String) {
objeto complementar {
operador
fun invoke(id: Int?, firstName: String?, lastName: String?) =
Pessoa(assertPositive(id, "id nulo ou negativo"),
assertValidName(firstName, "primeiro nome inválido"),
assertValidName(lastName, "sobrenome inválido"))

private fun assertPositive(i: Int?,


mensagem: String): Int = quando (i) {
nulo -> lançar IllegalStateException (mensagem)
senão -> eu
}

diversão privada assertValidName(nome: String?,


mensagem: String): String = quando {
nome == nulo
|| nome.isEmpty()
|| nome[0].toInt() < 65
|| nome[0].toInt() > 91 ->
lançar IllegalStateException (mensagem)
senão -> nome
}
}
}

Mas se você deseja que seus programas sejam seguros, não deve lançar exceções.
Em vez disso, você deve usar contextos especiais, como Result para tratamento
de erros. Esse tipo de validação é abstraído no Result tipo. Tudo o que você pre-
cisa fazer é escrever as funções de validação, o que significa que você precisa es-
crever funções e usar referências de função. As funções de validação genéricas
podem ser colocadas no nível do pacote da seguinte forma:
fun isPositive(i: Int?): Boolean = i != null && i > 0

fun isValidName(nome: String?): Boolean =


nome != null && nome[0].toInt() >= 65 && nome[0].toInt() <= 91

Você pode então validar os dados:

class Person private constructor(val id: Int,


val primeiroNome: String,
val últimoNome: String) {

objeto complementar {
fun of(id: Int, firstName: String, lastName: String) =
Result.of (::isPositive, id, "ID negativo").flatMap { validId ->
Result.of(::isValidName, firstName, "Nome inválido")
.flatMap { validFirstName ->
Result.of(::isValidName, lastName, "Sobrenome inválido")
.map { validLastName ->
Pessoa(validId, validFirstName, validLastName)
}
}
}
}
}

Mas você também pode simplificar as coisas abstraindo mais do processo em fun-
ções mais gerais:
fun assertPositive(i: Int, message: String): Result<Int> =
Result.of(::isPositive, i, message)

fun assertValidName(nome: String, mensagem: String): Result<String> =


Result.of(::isValidName, nome, mensagem)

Você pode então criar um Person da seguinte forma:

fun of(id: Int, firstName: String, lastName: String) =


assertPositive(id, "ID negativo")
.flatMap { validId ->
assertValidName(firstName, "Nome inválido")
.flatMap { validFirstName ->
assertValidName(lastName, "Sobrenome inválido")
.map { validLastName ->
Pessoa(validId, validFirstName, validLastName)
}
}
}

A listagem a seguir mostra alguns exemplos de validaçãofunções.

Listagem 14.1 Exemplos de asserções funcionais

fun <T> assertCondition(valor: T, f: (T) -> Booleano): Resultado<T> =


assertCondition(valor, f,
"Erro de declaração: a condição deve ser avaliada como verdadeira")
fun <T> assertCondition(valor: T, f: (T) -> Booleano,
mensagem: String): Resultado<T> =
se (f(valor))
Resultado(valor)
outro
Result.failure(IllegalStateException(mensagem))

fun assertTrue(condição: Booleano,


mensagem: String = "Erro de declaração: a condição deve ser verdadeira"):
Resultado<Booleano> =
assertCondition(condição, { x -> x }, mensagem)

fun assertFalse(condição: Boolean,


mensagem: String = "Erro de declaração: a condição deve ser falsa"):
Resultado<Booleano> =
assertCondition(condição, { x -> !x }, mensagem)

fun <T> assertNotNull(t: T): Resultado<T> =


assertNotNull(t, "Erro de declaração: o objeto não deve ser nulo")

fun <T> assertNotNull(t: T, mensagem: String): Resultado<T> =


assertCondition(t, { x -> x != null }, mensagem)

fun assertPositive(valor: Int,


mensagem: String = "Erro de declaração: valor $valor deve ser positivo"):
Resultado<Int> =
assertCondition(valor, { x -> x > 0 }, mensagem)

fun assertInRange(valor: Int, min: Int, max: Int): Result<Int> =


assertCondition(value, { x -> x in min..(max - 1) },
"Erro de declaração: valor $ valor deve ser > $ min e < $ max")
fun assertPositiveOrZero(valor: Int,
mensagem: String = "Erro de declaração: valor $valor não deve ser < 0"):
Resultado<Int> =
assertCondition(valor, { x -> x >= 0 }, mensagem)

fun <A: Qualquer> assertType(elemento: A, clazz: Classe<*>): Resultado<A> =


assertType(elemento, clazz,
"Tipo errado: ${element.javaClass}, esperado: ${clazz.name}")

fun <A: Qualquer> assertType(elemento: A, clazz: Classe<*>,


mensagem: String): Resultado<A> =
assertCondition(elemento, { e -> e.javaClass == clazz }, mensagem)

14.2 Novas tentativas para funções e efeitos

Impurofunções e efeitos muitas vezes devem ser repetidos se não forem bem-su-
cedidos na primeira chamada. Não ter sucesso geralmente significa lançar uma
exceção. Mas repetir uma função quando uma exceção é lançada é tedioso e su-
jeito a erros.

Imagine que você está lendo um valor de algum dispositivo que pode lançar
um IOException se o dispositivo não estiver pronto. Você pode tentar novamente
três vezes com um atraso de 100 ms entre cada nova tentativa. A solução impera-
tiva é algo como

fun get(caminho: String): String = ①


Random().nextInt(10).let {
quando {
it < 8 -> throw IOException("Erro ao acessar arquivo $path")
else -> "conteúdo de $path"
}
}

var tentativas = 0
var resultado: String? = nulo
(0 .. 3).forEach rt@ { ②
tentar {
resultado = get("/meu/caminho")
return@rt ③
} catch(e: IOException) {
if (novas tentativas < 3) {
Thread.sleep(100) ④
novas tentativas += 1
} outro {
jogar e ⑤
}
}
}
println(resultado)

① Simula uma função que lança uma exceção 80% das vezes

② Usado como parâmetro da função forEach; rt@ indica onde você deseja retornar
de dentro da função

③ Conforme indicado por return@rt, o retorno de dentro da função usada como pa-
râmetro da função forEach é acionado quando a chamada para get é bem-
sucedida.
④ Se get lança uma exceção e as tentativas são menores que 3, uma nova tentativa é
tentada após aguardar 100 ms.

⑤ Se uma exceção for lançada e as novas tentativas não forem menores que 3, a ex-
ceção será lançada novamente.

Este código é ruim por vários motivos:

Você é forçado a usar var referências.


Você tem que usar um tipo anulável para o resultado.
Não é absolutamente reutilizável, embora o conceito de repetição seja algo
usado com frequência.

O que você precisa é de uma retry funçãoque toma como parâmetros o seguinte:

A função para tentar novamente


Um número máximo de tentativas
Um valor de atraso entre novas tentativas

Esta função não deve relançar nenhuma exceção. Em vez disso, ele deve retornar
um arquivo Result . Aqui está sua assinatura:

fun <A, B> repetir(f: (A) -> B,


vezes: Inter,
atraso: Longo = 10): (A) -> Resultado<B>

Usando esta função, você pode escrever


val functionWithRetry = retry(::get, 10, 100)
functionWithRetry("/meu/arquivo.txt")
.forEach({ println(it) }, { println(it.message) })

Você pode obter esse resultado de várias maneiras diferentes. Uma maneira seria
usar uma dobra com curto-circuito, dobrando o intervalo de 0 para ( número má-
ximo de tentativas ), mas escapando assim que uma chamada para a get fun-
çãosucesso. Você pode fazer isso facilmente usando um rótulo e a fold função
Kotlin padrãoem um intervalo Kotlin, como

fun <A, B> retry(f: (A) -> B, times: Int, delay: Long = 10) = rt@ { a: A ->
(0 .. vezes).fold("Não executado") { _, n ->
tentar {
print("Tente $n: ")
return@rt "Sucesso $n: ${f(a)}"
} catch (e: Exceção) {
Thread.sleep(atraso)
"${e.mensagem}"
}
}
}

Por outro lado, isso não funcionará com a range funçãovocê desenvolveu usando
sua List classeno exercício 8.19. Parece ser devido a um bug no Kotlin e, de qual-
quer forma, não compila. 1   Uma maneira de resolver o problema é usar corecur-
sion explícita, como você aprendeu no capítulo 4. Como sempre, isso implica defi-
nir uma função local auxiliar:
fun <A, B> repetir(f: (A) -> B,
vezes: Inter,
atraso: Longo = 10): (A) -> Resultado<B> {
fun retry(a: A, resultado: Resultado<B>, ​
e: Resultado<B>, ​
tms: Int): Resultado<B> =
result.orElse {
quando (tm) {
0 -> e
senão -> {
Thread.sleep(atraso)
// registra o número de tentativas
println("tente novamente ${vezes - tms}")
retry(a, Result.of { f(a) }, result, tms - 1)
}
}
}
return { a -> retry(a, Result.of { f(a) }, Result(), times - 1)}
}

Essa implementação usa uma função local que chama a si mesma recursivamente
com um número reduzido de novas tentativas até que esse número seja 0 ou a
chamada para a função seja bem- f sucedida. Você não pode tirar proveito da pa-
lavra- tailrec chave porque o Kotlin não vê essa função como recursiva. Isso
não é um problema, no entanto, porque o número de novas tentativas será baixo.
A println instrução é incluída apenas para permitir que você veja o que
acontece.

A função local é inicialmente chamada com Result.of { f(a) } seu parâme-


tro, o que é um tanto incomum. Geralmente, você chama a função local com os
mesmos parâmetros da função principal, além de outros adicionais. Aqui, o caso
de uso é um pouco especial porque você não deseja um atraso inicial.

Com esta função, você pode transformar qualquer função em uma função com re-
petição automática. Você também pode usar esta função com efeitos puros (retor-
nando Unit ) como no seguinteexemplo:

divertido show(mensagem: String) =


Random().nextInt(10).let {
quando {
it < 8 -> throw IllegalStateException("Falha !!!")
else -> println(mensagem)
}
}

fun main(args: Array<String>) {


retry(::show, 10, 20)("Hello, World!").forEach(onFailure =
{ println(it.message) })
}

14.3 Lendo as propriedades de um arquivo

A maioriaaplicativos de software são configurados usando arquivos de proprie-


dade que são lidos na inicialização. As propriedades são pares chave/valor, e as
chaves e os valores são escritos como strings. Qualquer que seja o formato de pro-
priedade escolhido (chave = valor, XML, JSON, YAML e assim por diante), o pro-
gramador sempre terá que ler strings e transformá-las em objetos. Este processo é
tedioso e sujeito a erros.
Você pode usar uma biblioteca especializada para ler arquivos de propriedade,
mas se algo der errado, você se verá lançando exceções. Para obter um comporta-
mento mais funcional, você terá que escrever sua própria biblioteca. Esta biblio-
teca permite que você

Ler propriedades como strings


Leia as propriedades como valores numéricos de vários tipos
Leia as propriedades como enums ou até mesmo tipos arbitrários
Ler propriedades como coleções dos tipos acima
Leia as propriedades enquanto fornece valores padrão e mensagens de erro
significativas caso algo dê errado
Leia as propriedades sem nunca lançar exceções

14.3.1 Carregando o arquivo de propriedades

qualquer que sejaformato que você usa, o processo é exatamente o mesmo: ler o
arquivo e lidar com qualquer exceção que possa surgir nesse processo. A pri-
meira coisa a fazer é ler o arquivo de propriedades e retornar um
Result<Properties> , conforme mostrado na listagem a seguir.

Listagem 14.2 Lendo um arquivo de propriedade

class PropertyReader(configFileName: String) {

propriedades val internas: Result<Properties> = ①


Result.of { ②
MethodHandles.lookup().lookupClass() ③
.getResourceAsStream(configFileName) ④
.use { inputStream -> ⑤
Propriedades().let {
it.load(inputStream) ⑥
it ⑦
}
}
}
}

fun main(args: Array<String>) {


val propriedadeLeitor =
PropertyReader("/config.properties") ⑧
propertyReader.properties.forEach(onSuccess =
{ println(it) }, onFailure = { println(it) })
}

① A classe PropertyReader contém um Result<Properties> do qual você pode ler va-


lores de propriedade.

② A função Result.of retornará um sucesso se tudo correr bem ou uma falha se ocor-
rer uma exceção.

③ Isso permite obter uma referência à classe gerada, embora a função seja colocada
no nível do pacote.

④ Carrega o arquivo do classpath

⑤ Carrega o arquivo de propriedades, possivelmente causando uma IOException.

⑥ Carrega as propriedades do InputStream


⑦ A última linha do bloco retorna o valor. Você precisa especificar o valor usando o
nome do parâmetro padrão “it” porque a linha anterior retorna um booleano.

⑧ Aqui, o arquivo é colocado na raiz do classpath.

Se o arquivo não for encontrado, a use função não lançará um IOException mas
retorna um null inputStream , causando um NullPointerException . Esta
função garante que o fluxo será fechado em qualquercaso.

OBSERVAÇÃO Nesta listagem, se você usar o Intellij, precisará reconstruir o pro-


jeto antes de executar o exemplo. Executá-lo sem reconstruir cria as classes, mas
não copia os recursos para o diretório de saída. Isso ocorre porque você carrega o
arquivo de propriedade do caminho de classe. Ele pode, no entanto, ser carregado
de qualquer lugar no disco ou lido de um URL remoto ou de qualquer outra fonte.

14.3.2 Lendo propriedades como strings

Quandotrabalhando com arquivos de propriedades, o caso de uso mais simples


consiste em ler as propriedades como strings. Isso parece simples, mas esteja ci-
ente de que o seguinte não funcionará:

properties.map { it.getProperty("nome") }

Se a propriedade não existir, a getProperty funçãoretorna null , o que resulta


em um Success("null") . A Properties classe pode ser construída com uma
lista de propriedades padrão e a getProperty própria função pode ser chamada
com um valor padrão. Mas nem todas as propriedades têm valores padrão. Para
lidar com esse problema, você precisa usar a flatMap funçãojuntamente com
Result.of :

fun readProperty(nome: String) =


properties.flatMap {
Resultado de {
it.getProperty(nome)
}
}

Agora, digamos que você tenha um arquivo de propriedades no classpath que


contenha as seguintes propriedades:

host=acme.org
porta=6666
nome=
temperatura = 71,3
preço=$45
lista=34,56,67,89
person=id:3;firstName:Jeanne;lastName:Doe
id=3
tipo=SERIAL

Esse arquivo é chamado config.properties e colocado na raiz do classpath.


Você pode acessar suas propriedades de forma segura com este código:

fun main(args: Array<String>) {


val propertyReader = PropertyReader("/config.properties")
propertyReader.readProperty("host")
.forEach(onSuccess = { println(it) }, onFailure = { println(it) })
propertyReader.readProperty("nome")
.forEach(onSuccess = { println(it) }, onFailure = { println(it) })
propertyReader.readProperty("ano")
.forEach(onSuccess = { println(it) }, onFailure = { println(it) })
}

Dado o seu arquivo de propriedades, você obterá o seguinte resultado:

acme.org java.lang.NullPointerException

A primeira linha corresponde à host propriedade, qual é correto. A segunda li-


nha corresponde à name propriedade. É uma string vazia, que pode ou não estar
correta - você não sabe. Isso depende se o nome é opcional do ponto de vista
comercial.

A terceira linha corresponde à year propriedade ausente, mas a mensagem não é


informativa. Está contido em um Result<String> que pode ser atribuído a uma
year variável para que você saiba qual propriedade está faltando. Mas seria me-
lhor ter o nome da propriedade como parte da mensagem. Vamos tornar esta
mensagem de erro maisútil.

14.3.3Produzindo melhores mensagens de erro

oproblema que você está enfrentando aqui é um bom exemplo do que nunca de-
veria acontecer. Kotlin depende da biblioteca padrão Java, então você tem certeza
de que tudo correrá conforme o esperado. Em particular, você espera que, se um
arquivo não for encontrado ou se não puder ser lido, você receberá uma extensão
IOException . Você pode até esperar ser informado sobre o caminho completo
do arquivo, pois um arquivo ausente geralmente é um arquivo que não está no lu-
gar certo. Uma boa mensagem de erro nesse caso seria “ I am looking for
file 'abc' in location 'xyz' but can't find it. ” Agora, observe o có-
digo do getResourceAsStream método Java:

public InputStream getResourceAsStream(String name) {


URL url = getResource(nome);
tentar {
url de retorno! = null ? url.openStream() : nulo;
} catch (IOException e) {
retornar nulo;
}
}

Sim, é assim que o Java é escrito. A conclusão é que você nunca deve chamar um
método da biblioteca padrão Java sem olhar o código correspondente!

O Javadoc diz que o método retorna “Um fluxo de entrada para ler o recurso ou
null se o recurso não puder ser encontrado”. Isso significa que muitas coisas po-
dem dar errado. Pode ocorrer um `IOException erro se o arquivo não for en-
contrado ou se houver um problema durante a leitura. Ou o nome do arquivo
pode ser nulo. Ou o getResource métodopoderia lançar uma exceção ou retor-
nar null . (Veja o código desse método para ver o que quero dizer.)

O mínimo que você deve fazer é fornecer uma mensagem diferente para cada
caso. E, apesar do fato de que IOException é improvável que um seja lançado,
você ainda deve lidar com esse caso, bem como com o caso geral de uma exceção
inesperada, conforme mostrado na listagem a seguir.

Listagem 14.3 Produzindo mensagens de erro específicas

// as propriedades agora podem ser privadas


propriedades val privadas: Resultado<Propriedades> =
tentar {
MethodHandles.lookup().lookupClass()
.getResourceAsStream(configFileName)
.use { inputStream ->
quando (inputStream) {
nulo ->
Result.failure("Arquivo $configFileName não encontrado no classpath")
senão -> Propriedades().let {
it.load(inputStream)
Resultado(isso)
}

}
}
} catch (e: IOException) {
Result.failure("IOException lendo o recurso de classpath $configFileName")
} catch (e: Exceção) {
Result.failure("Exceção: ${e.message} " +
" ao ler o recurso classpath $configFileName")
}

Se o arquivo não for encontrado, a mensagem é


Arquivo /config.properties não encontrado no classpath

Você também precisa lidar com mensagens de erro relacionadas à propriedade.


Ao usar um código como este

val ano: Resultado<String> = propertyReader.readProperty(propriedades, "ano")

é claro que se você conseguir o NullPointerException mensagem de erro, sig-


nifica que o year imóvel não foi encontrado. Mas no exemplo a seguir, a mensa-
gem não fornece nenhuma informação sobre qual propriedade estava faltando:

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

fun main(args: Array<String>) {


val propertyReader = PropertyReader("/config.properties")
val pessoa = propertyReader.readProperty("id")
.map(String::toInt)
.flatMap { id ->
propertyReader.readProperty("firstName")
.flatMap { firstName ->
propertyReader.readProperty("lastName")
.map { lastName -> Pessoa(id, firstName, lastName) }
}
}
person.forEach(onSuccess = { println(it) }, onFailure = { println(it) })
}
Para resolver este problema, você tem várias opções à sua disposição. O mais sim-
ples é mapear a falha no readProperty helperfunção da
PropertyReader classe:

fun readProperty(nome: String) =


properties.flatMap {
Resultado de {
it.getProperty(nome)
}.mapFailure("Propriedade \"$name\" não encontrada")
}

O exemplo anterior produz a seguinte mensagem de erro, indicando claramente


que a id propriedadenão estava presente no arquivo de propriedades:

java.lang.RuntimeException: Propriedade "firstName" não encontrada

Outra fonte potencial de falha é um erro de análise ao converter a id proprieda-


devalor da string em um número inteiro. Por exemplo, se a propriedade foi

id=três

a mensagem de erro seria

java.lang.NumberFormatException: Para string de entrada: "três"


Isso não fornece informações suficientes porque é a mensagem de erro Java pa-
drão para um erro de análise. A maioria das mensagens de erro Java padrão são
assim. É como um arquivo NullPointerException . Diz que uma referência foi
encontrada para ser null , masnão diz qual. O que você precisa é o nome da pro-
priedade que causou a exceção, algo assim:

propertyReader.readProperty("id")
.map(String::toInt)
.mapFailure("Formato inválido para propriedade \"id\": ???")

Mas você tem que escrever o nome da propriedade duas vezes, e seria útil substi-
tuir ??? pelo valor encontrado. (Isso não é possível porque o valor já está per-
dido.) Como você terá que analisar os valores de propriedade para todas as pro-
priedades não string, você deve abstrair isso dentro da PropertyReader classe.
Para fazer isso, primeiro renomeie a readProperty função:

fun readAsString(nome: String) =


properties.flatMap {
Resultado de {
it.getProperty(nome)
}.mapFailure("Propriedade \"$name\" não encontrada")
}

Em seguida, você adicionará uma readAsInt função:

fun readAsInt(nome: String): Result<Int> =


readAsString(nome).flatMap {
tentar {
Resultado(it.toInt())
} catch (e: NumberFormatException) {
Result.failure<Int>(
"Valor inválido ao analisar a propriedade '$name' para Int: '$it'")
}
}

Agora você não precisa se preocupar com erros ao converter para números
inteiros:

val pessoa = propertyReader.readAsInt("id")


.flatMap { id ->
propertyReader.readAsString("firstName")
.flatMap { firstName ->
propertyReader.readAsString("lastName")
.map { lastName -> Pessoa(id, firstName, lastName) }
}
}
person.forEach(onSuccess = { println(it) }, onFailure = { println(it) })

Se uma exceção for lançada durante a análise da id propriedade, tupegue

java.lang.IllegalStateException:
Valor inválido ao analisar a propriedade 'id' para Int: 'três'")

14.3.4 Lendo propriedades como listas

Vocêpode fazer o mesmo que você fez para números inteiros para outros tipos nu-
méricos, como Long ou Double . Você pode até fazer muito mais do que isso. Por
exemplo, você pode ler propriedades como listas como esta:

lista=34,56,67,89

Você só precisa adicionar uma função especializada para lidar com este caso. Para
isso, você pode usar a seguinte função para ler uma propriedade como uma lista
de números inteiros:

fun readAsIntList(nome: String): Result<List<Int>> =


readAsString(nome).flatMap {
tentar {
Result(fromSeparated(it, ",").map(String::toInt))
} catch (e: NumberFormatException) {
Result.failure<List<Int>>(
"Valor inválido ao analisar a propriedade '$name' para List<Int>: '$it'")
}
}

Este código usa a fromSeparated função definida na List classeque você en-
contrará no módulo com.fpinkotlin.common . Esse módulo está disponível no
código que acompanha este livro ( https://github.com/pysaumont/fpinkotlin ). Você
pode alterar o código para usar a função padrão Kotlin List alterando uma
linha:

fun readAsIntList(nome: String): Result<List<Int>> =


readAsString(nome).flatMap {
tentar {
// A próxima linha usa a lista Kotlin
Result(it.split(",").map(String::toInt))
} catch (e: NumberFormatException) {
Result.failure<List<Int>>(
"Valor inválido ao analisar a propriedade '$name' para List<Int>: $it")
}
}

Mas você pode fazer muito mais! Você pode ler uma propriedade como uma lista
de quaisquer valores numéricos fornecendo a função de conversão:

fun <T> readAsList(nome: String, f: (String) -> T): Result<List<T>> =


readAsString(nome).flatMap {
tentar {
Result(fromSeparated(it, ",").map(f))
} catch (e: Exceção) {
Result.failure<List<T>>(
"Valor inválido ao analisar a propriedade '$name' para List: $it")
}
}

E você pode definir funções para todos os tipos de formatos de número em termos
de readAsList :

fun readAsIntList(nome: String): Result<List<Int>> =


readAsList(nome, String::toInt)

fun readAsDoubleList(nome: String): Result<List<Double>> =


readAsList(nome, String::toDouble)
fun readAsBooleanList(nome: String): Result<List<Boolean>> =
readAsList(nome, String::toBoolean)

Um caso de uso frequente consiste na leitura de uma propriedade como um


enum valor, que é um caso particular de leitura de uma propriedade como
qualquermodelo.

14.3.5Lendo valores de enumeração

Vocêpode primeiro criar uma função para converter uma propriedade em qual-
quer tipo T , levando uma função de String para Result<T>:

fun <T> readAsType(f: (String) -> Resultado<T>, nome: String) =


readAsString(nome).flatMap {
tentar {
ajuste)
} catch (e: Exceção) {
Resultado.falha<T>(
"Valor inválido ao analisar a propriedade '$name': '$it'")
}
}

Agora você pode criar uma readAsEnum função em termos de readAsType :

em linha
fun <reified T: Enum<T>> readAsEnum(nome: String,
enumClass: Classe<T>): Resultado<T> {
val f: (String) -> Resultado<T> = {
tentar {
valor val = enumValueOf<T>(it)
Resultado(valor)
} catch (e: Exceção) {
Result.failure("Erro ao analisar a propriedade '$name': " +
"valor '$it' não pode ser analisado para ${enumClass.name}.")
}
}
return readAsType(f, nome)
}

Observe que isso reified significa que o tipo T deve estar acessível em tempo
de execução. Ao contrário de Java, Kotlin permite acessar parâmetros de tipo em
tempo de execução usando a palavra-chave reified , para que não seja apagado.
Essa possibilidade só é acessível em funções declaradas com inline , o que signi-
fica que o compilador pode copiar o código da função no site da chamada, em vez
de referenciar o código original. Isso aumenta o tamanho do código compilado.

Dada a seguinte propriedade

tipo=SERIAL

e o seguinte enum

classe enum Tipo {SERIAL, PARALLEL}

agora você pode ler a propriedade usando o seguinte código:


val type = propertyReader.readAsEnum("type", Type::class.java)

Até agora você leu propriedades como String , Int , Double , Boolean , listas
ou enums. Também pode ser interessante ler propriedades como objetos arbitrá-
rios. Para isso, você terá que escrever as propriedades do objeto em uma espécie
de forma serializada no arquivo de propriedades e depois carregar essas proprie-
dades e desserializareles.

14.3.6Lendo propriedades de tipos arbitrários

Vocêpode usar a getAsType função para ler uma propriedade como qualquer
tipo. Por exemplo, você pode ler a seguinte propriedade para obter um Person :

person=id:3,firstName:Jane,lastName:Doe

Tudo o que você precisa fazer é fornecer uma função de String para
Result<Person> . Esta função deve ser capaz de criar um Person a partir da
string "id:3,firstName:Jane,lastName:Doe" . Para simplificar seu uso, você
pode criar uma readAsPerson função. Mas como é específico do tipo, você não
deve colocá-lo dentro da PropertyReader classe. Uma solução melhor é adicio-
nar uma função usando a PropertyReader e o nome da propriedade como argu-
mentos para a Person classe.

Existem várias maneiras de implementar essa função. Uma maneira é obter a pro-
priedade como uma lista e depois dividir cada elemento, colocando os pares
chave/valor em um mapa. Seria então fácil criar um a Person partir deste mapa.
Outra maneira seria criar um segundo PropertyReader que lê a string depois de
substituir as vírgulas por caracteres de nova linha. A listagem a seguir mostra a
Person classecom duas funções específicas para construir instâncias de uma
string de propriedade.

Listagem 14.4 Métodos que leem propriedades como objetos ou listas de objetos

classe de dados Person(val id: Int,


val primeiroNome: String,
val últimoNome: String) {

objeto complementar {
fun readAsPerson(propertyName: String,
PropertyReader: PropertyReader): Resultado<Pessoa> {
val rString = propertyReader.readAsPropertyString(propertyName)
val rPropReader = rString.map { stringPropertyReader(it) }
return rPropReader.flatMap { readPerson(it) }
}

fun readAsPersonList(propertyName: String,


PropertyReader: PropertyReader):
Resultado<Lista<Pessoa>> =
propertyReader.readAsList(propertyName, { it }).flatMap { list ->
sequence(list.map { s ->
lerPessoa(PropriedadeLeitor
.stringPropertyReader(PropertyReader.toPropertyString(s)))
})
}

private fun readPerson(propReader: PropertyReader): Resultado<Pessoa> =


propReader.readAsInt("id")
.flatMap { id ->
propReader.readAsString("firstName")
.flatMap { firstName ->
propReader.readAsString("lastName")
.map { lastName -> Pessoa(id, firstName, lastName) }
}
}
}
}

Com a readAsPersonList função, você pode ler as propriedades do vetor escri-


tas da seguinte forma:

empregados=\
id:3;firstName:Jane;lastName:Doe,\
id:5;primeiroNome:Paul;últimoNome:Smith,\
id:8;primeiroNome:Mary;últimoNome:Winston

Essas funções requerem algumas mudanças na PropertyReader classe,como


mostra a próxima listagem.

Listagem 14.5 Funções adicionadas à PropertyReader classe

class PropertyReader(
private val properties: Result<Properties>, ①
private val source: String) { ②

...

fun readAsPropertyString(propertyName: String):


Resultado<String> = ③
readAsString(propertyName).map { toPropertyString(it) }

objeto complementar {

fun toPropertyString(s: String): String =


s.replace(";", "\n") ④

privado
fun readPropertiesFromFile(configFileName: String):
Resultado<Propriedades> = ⑤
tentar {
MethodHandles.lookup().lookupClass()
.getResourceAsStream(configFileName)
.use { inputStream ->
quando (inputStream) {
null -> Result.failure(
"Arquivo $configFileName não encontrado no classpath")
senão -> Propriedades().let {
it.load(inputStream)
Resultado(isso)
}

}
}
} catch (e: IOException) {
Resultado.falha(
"IOException lendo o recurso de caminho de classe $configFileName")
} catch (e: Exceção) {
Result.failure("Exceção: ${e.message}lendo classpath"
+ "recurso $configFileName")
}
diversão privada readPropertiesFromString(propString: String):
Resultado<Propriedades> = ⑥
tentar {
StringReader(propString).use { leitor ->
val propriedades = Propriedades()
properties.load(leitor)
Resultado(propriedades)
}
} catch (e: Exceção) {
Result.failure("Exceção ao ler string de propriedades " +
"$propString: ${e.mensagem}")
}

fun filePropertyReader(fileName: String):


PropertyReader = ⑦
PropertyReader(readPropertiesFromFile(fileName),
"Arquivo: $arquivo")

fun stringPropertyReader(propString: String):


PropertyReader = ⑧
PropertyReader(readPropertiesFromString(propString),
"String: $propString")
}
}

① Constrói o PropertyReader com um Result<Properties>

② Registra a fonte para ser usada em mensagens de erro


③ Converte um único valor de propriedade em uma string de propriedade que pode
ser usada como entrada para um PropertyReader aninhado

④ Lê uma propriedade e converte o valor em uma string de propriedade

⑤ A implementação original para ler um arquivo de propriedade

⑥ Uma nova função para ler as propriedades de uma string de propriedade

⑦ Cria um PropertyReader a partir de um nome de arquivo

⑧ Cria um PropertyReader a partir de uma string de propriedade

Você pode fazer o mesmo para arquivos de propriedades XML ou para outros for-
matos, como JSONouYAML.

14.4 Convertendo um programa imperativo: o leitor de XML

Escritanovos programas funcionais para qualquer tarefa que você tenha que rea-
lizar é empolgante, mas a maioria dos desenvolvedores geralmente não tem
tempo para isso. Freqüentemente, você desejará usar programas imperativos
existentes em seu próprio código. Este é o caso toda vez que você deseja usar uma
biblioteca existente.

Você pode achar mais interessante começar do zero e construir uma solução com-
pletamente nova e 100% funcional. Mas você tem que ser realista. Geralmente,
você não tem tempo ou orçamento para fazer isso e terá que usar bibliotecas não
funcionais existentes cheias de null exceções, lançamento de exceções e funções
impuras que alteram seus parâmetros no mundo exterior.

Como você logo descobrirá, quando estiver familiarizado com as técnicas funcio-
nais, será difícil voltar ao antigo estilo de codificação imperativo. A solução geral-
mente é construir um wrapper funcional fino em torno dessas bibliotecas impera-
tivas. Como exemplo, vamos examinar uma biblioteca comum para leitura de ar-
quivos XML, JDOM 2.0.6. Esta é a biblioteca Java mais comumente usada para esta
tarefa e é perfeitamente utilizável com Kotlin.

14.4.1 Passo 1: A solução imperativa

vamoscomece com o programa de exemplo na listagem 14.6 . Este programa vem


de um dos inúmeros sites que propõem tutoriais sobre como usar o JDOM (
http://www.mkyong.com/java/how-to-read-xml-file-in-java-jdom-example/ ). Esco-
lhi este exemplo porque é mínimo e cabe facilmente no livro. Este é um programa
Java usando uma biblioteca Java.

Listagem 14.6 Lendo dados XML com JDOM: versão Java

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import java.io.File;
importar java.io.IOException;
importar java.util.List;

public class ReadXmlFile {


public static void main(String[] args) {
Construtor SAXBuilder = new SAXBuilder();
Arquivo xmlFile = new File("path_to_file");
tentar {
Documento documento = (Documento) builder.build(xmlFile);
Elemento rootNode = document.getRootElement();
List list = rootNode.getChildren("staff");
for (int i = 0; i < list.size(); i++) {
Nó do elemento = (Elemento) list.get(i);
System.out.println("Nome : " +
node.getChildText("nome"));
System.out.println("\tSobrenome: " +
node.getChildText("sobrenome"));
System.out.println("\tNome do Nick: " +
node.getChildText("email"));
System.out.println("\tSalário : " + node.getChildText("salário"));
}
} catch (IOException io) {
System.out.println(io.getMessage());
} catch (JDOMException jdomex) {
System.out.println(jdomex.getMessage());
}
}
}

Este programa Java pode ser facilmente reescrito em Kotlin imperativo, como
você pode ver na listagem a seguir.

Listagem 14.7 Lendo dados XML com JDOM: versão imperativa do Kotlin
importar org.jdom2.JDOMException
import org.jdom2.input.SAXBuilder
importar java.io.File
importar java.io.IOException

/**
* Não testável, lança exceções.
*/
fun main(args: Array<String>) {

construtor val = SAXBuilder()


val xmlFile = File("/path/to/file.xml") // Corrige o caminho

tentar {
val document = builder.build(xmlFile)
val rootNode = document.rootElement
lista val = rootNode.getChildren("staff")

lista.paraCada {
println("Nome: ${it.getChildText("firstName")}")
println("\tSobrenome: ${it.getChildText("lastName")}")
println("\tEmail: ${it.getChildText("email")}")
println("\tSalário: ${it.getChildText("salário")}")
}
} catch (io: IOException) {
println(io.message)
} catch (e: JDOMException) {
println(e.mensagem)
}
}
O arquivo de dados usado com este exemplo é mostrado na listagem a seguir.

Listagem 14.8 O arquivo XML a ser lido

<?xml versão="1.0"?>
<empresa>
<pessoal>
<firstName>Paulo</firstName>
<lastName>Smith</lastName>
<email>paul.smith@acme.com</email>
<salário>100000</salário>
</staff>
<pessoal>
<firstName>Maria</firstName>
<lastName>Colson</lastName>
<email>mary.colson@acme.com</email>
<salário>200000</salário>
</staff>
</empresa>

Vejamos os benefícios que você obtém ao reescrever este exemplo de maneira


funcional. O primeiro problema que você pode encontrar é que nenhuma parte
do programa pode ser reutilizada. É apenas um exemplo, mas mesmo como exem-
plo, deve ser escrito de forma reutilizável para que seja pelo menos testável. Aqui,
a única maneira de testar o programa é olhar para o console, que exibirá o resul-
tado esperado ou uma mensagem de erro. Como você verá, pode até exibir um re-
sultado incorreto.
14.4.2 Passo 2: Tornando um programa imperativo mais funcional

Para determinar as funções necessárias e tornar este programa mais funcional,


comece por

Listando as funções fundamentais que você precisa


Escrever essas funções como unidades autônomas, reutilizáveis ​e testáveis
Codificando o exemplo compondo essas funções

As principais funções que você precisa farão o seguinte:

Leia um arquivo e retorne o conteúdo como uma string XML


Converter a string XML em uma lista de elementos
Converter uma lista de elementos em uma lista de representações de string
desses elementos

Você também precisará de um efeito para exibir a lista de strings na tela do


computador.

OBSERVAÇÃO Esta descrição é adequada apenas para um arquivo pequeno que


pode ser carregado inteiramente na memória.

A primeira função necessária, que lê um arquivo e retorna o conteúdo como uma


string XML, pode ser implementada da seguinte forma:

fun readFile2String(caminho: String): Resultado<String>

Esta função não lança nenhuma exceção; ele retorna um Result<String> .


A segunda função converte uma string XML em uma lista de elementos, portanto,
ela precisa saber o nome do elemento XML raiz. Tem a seguinte assinatura:

fun readDocument(rootElementName: String,


stringDoc: String): Resultado<Lista<Elemento>>

A terceira função recebe uma lista de elementos como seu argumento e retorna
uma lista de representações de string desses elementos. Isso é implementado por
uma função com a seguinte assinatura:

fun toStringList(list: List<Element>, format: String): List<String>

Eventualmente você precisará aplicar um efeito aos dados, então você terá que
defini-lo com a seguinte assinatura:

fun <A> processList(lista: List<A>)

Essa decomposição em funções não parece muito diferente do que você poderia
fazer na programação imperativa. Afinal, é uma boa prática decompor programas
imperativos em funções com uma única responsabilidade cada. Mas é mais dife-
rente do que pode parecer.

Observe que a readDocument funçãotoma como primeiro parâmetro uma string


que é retornada por uma função que poderia (no mundo imperativo) lançar uma
exceção. Você precisará lidar com a função adicional:
fun getRootElementName(): Resultado<String>

Da mesma forma, o caminho do arquivo pode ser retornado pelo mesmo tipo de
função:

fun getXmlFilePath(): Resultado<String>

O importante a observar é que os tipos de argumento e os tipos de retorno dessas


funções não correspondem! Esta é a tradução explícita do fato de que as versões
imperativas dessas funções seriam parciais, o que significa que possivelmente
lançariam exceções.As funções que lançam exceções não são bem compostas. Em
contraste, suas funções se compõem perfeitamente.

Compondo as funções e aplicando um efeito

Apesaros tipos de argumento e retorno não correspondem, você pode compor es-
sas funções facilmente usando um padrão de compreensão como este:

const val format = "Nome: %s\n" +


"\tSobrenome: %s\n" +
"\tE-mail: %s\n" +
"\tSalário: %s"

fun main(args: Array<String>) {


val path = getXmlFilePath()
val rDoc = path.flatMap(::readFile2String)
val rRoot = getRootElementName()
val resultado = rDoc.flatMap { doc ->
rRoot.flatMap { rootElementName ->
readDocument(rootElementName, doc)
}.map { lista ->
toStringList(lista, formato)
}
}
...
}

Para exibir o resultado, aplique o efeito correspondente:

resultado.forEach(onSuccess =
{ processList(it) }, onFailure = { it.printStackTrace() })

Esta versão funcional do programa é muito mais limpa e totalmente testável. Ou,
pelo menos, será quando você tiver implementado todos osfunções.

Implementando as funções

SuaO programa é relativamente elegante, mas você ainda precisa implementar as


funções e os efeitos que está usando para fazê-lo funcionar. A boa notícia é que
cada função é simples e pode ser facilmente testada.

Primeiro, você implementará o getXmlFilePath e getRootElementName fun-


ções. Em nosso exemplo, essas são constantes que seriam substituídas por uma
implementação específica em um aplicativo real:
fun getRootElementName(): Resultado<String> =
Result.of { "staff" } // Simulando uma computação que pode falhar.

fun getXmlFilePath(): Resultado<String> =


Result.of { "file.xml" } // <- ajustar caminho

Então você precisa implementar a readFile2String função. Aqui está uma das
muitas implementações possíveis:

fun readFile2String(caminho: String): Result<String> =


Result.of { File(path).readText() }

Em seguida, você precisa implementar a readDocument função. Esta função


toma como parâmetro uma string XML contendo os dados XML e o nome do ele-
mento raiz:

fun readDocument(rootElementName: String,


stringDoc: String): Resultado<Lista<Elemento>> =
SAXBuilder().let { construtor ->
tentar {
documento val =
builder.build(StringReader(stringDoc)) ①
val rootElement = document.rootElement ②
Result(List(*rootElement.getChildren(rootElementName)
.toTypedArray())) ③
} catch (io: IOException) {
Result.failure("Nome de elemento raiz inválido '$rootElementName' "
+ "ou dados XML $stringDoc: ${io.message}")
} catch (jde: JDOMException) {
Result.failure("Nome de elemento raiz inválido '$rootElementName' "
+ "ou dados XML $stringDoc: ${jde.message}")
} catch (e: Exceção) {
Result.failure("Erro inesperado ao ler dados XML "
+ "$stringDoc: ${e.mensagem}")
}
}

① Pode lançar um NullPointerException

② Pode lançar uma IllegalStateException

③ O * no início da expressão indica que o array resultante deve ser usado como um
vararg e não como um único objeto.

Você primeiro pega IOException (queé improvável que seja lançado porque
você está lendo de uma string) e JDOMException , ambos são exceções verifica-
das e retornam uma falha com a mensagem de erro correspondente. Mas olhando
para o código JDOM (lembre-se de que ninguém deve chamar um método de bibli-
oteca sem antes ver como ele é implementado), você vê que o código pode lançar
um IllegalStateException ou um NullPointerException . Mais uma vez,
você tem que pegar Exception . a toStringList funçãomapeia a lista para
uma função responsável pela conversão:

fun toStringList(list: List<Element>, format: String): List<String> =


list.map { e -> processElement(e, formato) }

fun processElement(elemento: Elemento, formato: String): String =


String.format(formato, elemento.getChildText("primeiroNome"),
element.getChildText("lastName"),
element.getChildText("e-mail"),
element.getChildText("salário"))

Finalmente, você precisa implementar o efeito que será aplicado ao resultado:

fun <A> processList(lista: List<A>) = list.forEach(::println)

14.4.3 Passo 3: Tornando o programa ainda mais funcional

Seu programa agora é muito mais modular e mais testável, e suas partes são reuti-
lizáveis. Mas você ainda pode fazer melhor. Você ainda está usando quatro ele-
mentos não funcionais:

O caminho do arquivo
O nome do elemento raiz
O formato usado para converter os elementos em string
O efeito aplicado ao resultado

Por não funcional, quero dizer que esses elementos são acessados ​diretamente da
implementação de suas funções, tornando-os transparentes de forma não referen-
cial. Para tornar seu programa totalmente funcional, você deve tornar esses ele-
mentos parâmetros de seu programa.

a processElement funçãotambém usou dados específicos na forma de nomes de


elementos, que correspondem aos parâmetros da string de formato usada para
exibi-los. Você pode substituir o parâmetro de formato por a Pair (a string de
formato e uma lista de parâmetros). Desta forma, a processElement função
passa a ser a seguinte:

fun toStringList(lista: List<Element>,


format: Pair<String, List<String>>): List<String> =
list.map { e -> processElement(e, formato) }

fun processElement(elemento: Elemento,


formato: Pair<String, List<String>>): String {
val formatString = format.first
parâmetros val = format.second.map { element.getChildText(it) }
return String.format(formatString, *parameters.toArrayList().toArray())
}

Agora seu programa pode ser uma função pura, recebendo quatro argumentos e
retornando um novo programa executável (não funcional) como resultado. Esta
versão do programa é representada na listagem a seguir.

Listagem 14.9 O programa leitor de XML totalmente funcional

import com.fpinkotlin.common.List
import com.fpinkotlin.common.Result
import org.jdom2.Element
importar org.jdom2.JDOMException
import org.jdom2.input.SAXBuilder
importar java.io.File
importar java.io.FileInputStream
importar java.io.IOException
importar java.io.StringReader
fun readXmlFile(
sPath: () -> Resultado<String>,
sRootName: () -> Resultado<String>,
formato: Pair<String, List<String>>,
efeito: (List<String>) -> Unit): () -> Unit { ①
val path = sPath() ②
val rDoc = path.flatMap(::readFile2String)
val rRoot = sRootName() ③
val resultado = rDoc.flatMap { doc ->
rRoot.flatMap { rootElementName ->
readDocument(rootElementName, doc) }
.map { lista -> toStringList(lista, formato) }
}
Retorna {
result.forEach(onSuccess = { efeito(isso) },
onFailure = { it.printStackTrace() }) ④
}
}

fun readFile2String(caminho: String): Result<String> =


Result.of { File(path).readText() }

fun readDocument(rootElementName: String,


stringDoc: String): Resultado<Lista<Elemento>> =
SAXBuilder().let { construtor ->
tentar {
val document = builder.build(StringReader(stringDoc))
val rootElement = document.rootElement
Result(List(*rootElement.getChildren(rootElementName)
.toTypedArray()))
} catch (io: IOException) {
Result.failure("Nome de elemento raiz inválido '$rootElementName' "
+ "ou dados XML $stringDoc: ${io.message}")
} catch (jde: JDOMException) {
Result.failure("Nome de elemento raiz inválido '$rootElementName' "
+ "ou dados XML $stringDoc: ${jde.message}")
} catch (e: Exceção) {
Result.failure("Erro inesperado ao ler dados XML "
+ "$stringDoc: ${e.mensagem}")
}
}

fun toStringList(lista: List<Element>,


format: Pair<String, List<String>>): List<String> =
list.map { e -> processElement(e, formato) }

fun processElement(elemento: Elemento,


formato: Pair<String, List<String>>):
String { ⑤
val formatString = format.first
parâmetros val = format.second.map { element.getChildText(it) }
return String.format(formatString, *parameters.toArrayList().toArray())
}

① O caminho e o nome do elemento raiz agora são recebidos como funções constan-
tes. O formato inclui os nomes dos parâmetros e a função tem um efeito do tipo
(List<String>) -> Unit como um parâmetro adicional.

② Avalia as funções para obter os valores dos parâmetros

③ Parametriza o efeito a ser aplicado


④ A função retorna um programa, ou seja, uma função do tipo () -> Unidade, apli-
cando ao resultado o efeito recebido como parâmetro. Esta função lança exceções.
Não há nada melhor a fazer, pois é um efeito e não pode retornar um valor.

⑤ A função processElement não é mais específica.

Neste ponto, você pode testar este programa com o código do cliente mostrado na
listagem a seguir.

Listagem 14.10 O programa cliente para testar o leitor de XML

import com.fpinkotlin.common.List
import com.fpinkotlin.common.Result

fun <A> processList(lista: List<A>) = list.forEach(::println)

fun getRootElementName(): Resultado<String> =


Result.of { "staff" } // Simulando uma computação que pode falhar.

fun getXmlFilePath(): Resultado<String> =


Result.of { "/path/to/file.xml" } // <- ajustar caminho

private val format = Pair("Nome: %s\n" +


"\tSobrenome: %s\n" +
"\tE-mail: %s\n" +
"\tSalário : %s", List("primeiroNome", "últimoNome", "email", "salário"))

fun main(args: Array<String>) {


val program = readXmlFile( { getXmlFilePath() },
{ getRootElementName() },
formato, { processList(it) })
programa()
}

Este programa não é ideal porque você não lidou com o possível erro que pode
surgir de nomes de elementos inválidos. Por exemplo, se você usar um nome de
elemento errado como em

<empresa>
<pessoal>
<firstname></firstname>
<lastName>Smith</lastName>
<email>paul.smith@acme.com</email>
<salário>100000</salário>
</staff>
<pessoal>
<firstname>Maria</firstname>
<lastName>Colson</lastName>
<email>mary.colson@acme.com</email>
<salário>200000</salário>
</staff>
</empresa>

você obterá o seguinte resultado:

Nome: nulo
Apelido: Smith
e-mail: paul.smith@acme.com
Salário: 100.000
Nome: nulo
Apelido: Colson
e-mail: mary.colson@acme.com
Salário: 200.000

Você pode adivinhar qual é o erro vendo que todos os primeiros nomes são nu-
los. Seria melhor substituir a palavra null por uma mensagem explícita contendo
o nome do elemento incorreto. Um problema mais importante é que, se você es-
quecer um dos nomes de elemento na lista, obterá uma exceção da
String.format função devido ao seguinte código:

parâmetros val = format.second.map { element.getChildText(it) }


return String.format(formatString, *parameters.toArrayList().toArray())

Nesse código, a matriz de parâmetros terá apenas três elementos em vez dos qua-
tro esperados. Mas será difícil localizar a origem do erro da exceçãovestígio. Na
verdade, a verdadeira causa do problema é que você retirou todos os dados espe-
cíficos da readXmlFile função, como o nome do elemento raiz, o caminho do ar-
quivo e o efeito a ser aplicado, mas a processElement funçãoainda é específico
para o caso de uso de negócios do cliente. A readXmlFile função permite apenas
ler todos os elementos que são filhos diretos do elemento raiz, reunindo alguns
dos valores de seus elementos filhos diretos (aqueles cujos nomes são passados ​
junto com o formato).

Um terceiro problema é que a readXmlFile funçãorecebe dois argumentos do


mesmo tipo. Esta é uma fonte de erro se os argumentos forem trocados, o que não
será detectado pelo compilador. Você pode corrigir esse problema facilmente, en-
tão vamos lidar com isso a seguir. Depois disso, você corrigirá os dois primeiros
problemas.

14.4.4 Etapa 4: corrigindo o problema do tipo de argumento

oO terceiro problema é fácil de resolver usando a técnica de tipos de valor des-


crita no capítulo 3. Em vez de usar Result<String> argumentos, você pode usar
Result<FilePath> e Result<ElementName> . FilePath e ElementName são
classes de valor para valores de string, conforme mostrado aqui:

classe de dados FilePath private constructor(val value: Result<String>) {

objeto complementar {

operador divertido invocar(valor: String): FilePath =


FilePath(Result.of({ isValidPath(it) }, valor,
"Caminho de arquivo inválido: $valor"))

// Substitua pelo código de validação


diversão privada isValidPath(caminho: String): Boolean = true
}
}

a ElementName classeé semelhante, mas você precisa adicionar o código de vali-


dação se quiser que alguma validação aconteça. A maneira mais simples é verifi-
car o valor em uma expressão regular. Para usar esses novos tipos, a readXmlFi-
le funçãopode ser modificado da seguinte forma:
fun readXmlFile(sPath: () -> FilePath,
sRootName: () -> ElementName,
formato: Pair<String, List<String>>,
efeito: (List<String>) -> Unidade): () -> Unidade {
val path = sPath().value
val rDoc = path.flatMap(::readFile2String)
val rRoot = sRootName().value

Como você vê, as mudanças são mínimas. A classe do cliente também deve ser
modificada:

fun getRootElementName(): ElementName =


ElementName("staff") // Simulando uma computação que pode falhar.

fun getXmlFilePath(): FilePath =


FilePath("/caminho/para/arquivo.xml") // <- ajustar caminho

Com essas mudanças, agora é impossível mudar a ordem dos argumentos sem ser
avisado pelocompilador.

14.4.5Etapa 5: tornar a função de processamento de elementos um


parâmetro

odois problemas restantes podem ser resolvidos com uma única alteração: passar
a função de processamento de elementos como um parâmetro para o readXml-
File método. Dessa forma, essa função tem uma única tarefa: lê a lista de ele-
mentos de primeiro nível do arquivo, aplica uma função configurável a essa lista
e retorna o resultado. A principal diferença é que a função não produzirá mais
uma lista de strings e aplicará um efeito de string. Você precisará tornar a função
genérica. Isso significa apenas as seguintes alterações:

fun <T> readXmlFile(


sPath: () -> FilePath, ①
sRootName: () -> ElementName,
função: (Elemento) -> T, ②
efeito: (List<T>) -> Unidade): () -> Unidade { ③
val path = sPath().value
val rDoc = path.flatMap(::readFile2String)
val rRoot = sRootName().value
val resultado = rDoc.flatMap { doc ->
rRoot.flatMap { rootElementName ->
readDocument(rootElementName, doc) }
.map { lista -> lista.map(função) } ④
}
Retorna {
result.forEach(onSuccess = { efeito(isso) },
onFailure = { jogue })
}
}

① A função é genérica.

② O argumento de formato Pair<String, List<String>> desapareceu e um novo argu-


mento de função o substitui. Esta é a função que será aplicada para converter a
lista de elementos em uma lista de T.

③ O efeito a ser aplicado agora é parametrizado por List<T>.


④ As funções toStringList e processElement foram removidas. Eles são substituídos
por um aplicativo da função recebida.

O programa cliente agora pode ser modificado de acordo. Isso evita que você use
o Pair truque para passar a string de formato e a lista de nomes de parâmetros:

const val format = "Nome: %s\n" + ①


"\tSobrenome: %s\n" +
"\tE-mail: %s\n" +
"\tSalário: %s"

private val elementNames = ②


List("firstName", "lastName", "email", "salário")

diversão privada processElement(elemento: Elemento): String = ③


String.format(format, *elementNames.map { element.getChildText(it) }
.toArrayList()
.toArray())

fun main(args: Array<String>) {


val program = readXmlFile(::getXmlFilePath,
::getRootElementName,
::processElement, ④
::lista de processos)
...

① O formato agora está definido novamente como uma string simples.

② A lista de nomes de elementos também é definida separadamente.


③ A função processElement agora é implementada pelo cliente.

④ A função processElement é passada como um argumento.

O processList efeito não mudou. Agora cabe ao cliente fornecer uma função
para converter um elemento e um efeito para aplicar a esteelemento.

14.4.6Passo 6: Tratamento de erros em nomes de elementos

Agoravocê fica com o problema de erros acontecendo ao ler os elementos. A fun-


ção que é passada para a readXmlFile funçãoretorna um tipo bruto, o que signi-
fica que deveria ser uma função total, mas não é. Era uma função total no exem-
plo inicial porque um erro produziu a string nula. Agora que você está usando
uma função de Element para T , você pode usar Result<String> como a reali-
zação de T , mas isso não seria prático porque você acabaria com um
List<Result<T>> e teria que transformá-lo em um Result<List<T>> . Não é
grande coisa, mas isso definitivamente deve ser abstraído.

A solução é usar uma função de Element to Result<T> e usar a sequence fun-


ção para transformar o resultado em um arquivo Result<List<T>> . Aqui está a
nova função:

fun <T> readXmlFile(sPath: () -> FilePath,


sRootName: () -> ElementName,
função: (Elemento) -> Resultado<T>, ①
efeito: (List<T>) -> Unidade): () -> Unidade {
val path = sPath().value
val rDoc = path.flatMap(::readFile2String)
val rRoot = sRootName().value
val resultado = rDoc.flatMap { doc ->
rRoot.flatMap { rootElementName ->
readDocument(rootElementName, doc) }
.flatMap { lista ->
sequence(list.map(function)) } ②
}
Retorna {
result.forEach(onSuccess = { efeito(isso) },
onFailure = { jogue })
}
}
...

① A função recebida como argumento agora é uma função de Element para


Result<T>.

② O resultado é sequenciado, produzindo um Result<List<T>>. A função map deve


ser substituída por flatMap.

A única alteração adicional a ser feita é para lidar com o erro que pode ocorrer na
processElement função. A melhor abordagem é mais uma vez examinar o có-
digo do getChildText métodode JDOM. Este método é implementado da se-
guinte forma:

/**
* Retorna o conteúdo textual do elemento filho nomeado ou nulo se
* não existe tal criança. Este método é uma conveniência porque chamar
* <code>getChild().getText()</code> pode gerar uma NullPointerException.
*
* @param cname o nome da criança
* @return conteúdo de texto para o filho nomeado ou nulo se não houver tal filho
*/
public String getChildText(string final cname) {
elemento final filho = getChild(cname);
if (filho == nulo) {
retornar nulo;
}
return filho.getText();
}

Conforme você continua examinando o código para o getChild método, você


pode ver que este método não lançará nenhuma exceção, mas retornará null se
o elemento não existir. Você pode modificar sua processElement funçãoassim:

fun processElement(elemento: Elemento): Result<String> = ①


tentar {
Result(String.format(format, *elementNames.map {
getChildText(elemento, it) }
.toArrayList()
.toArray()))
} catch (e: Exceção) {
Resultado.falha(
"Exceção ao formatar elemento." + ②
"Causa provável é um nome de elemento ausente no elemento " +
"lista $elementNames")
}

divertido getChildText(elemento: Elemento,


nome: String): String = ③
elemento.getChildText(nome) ?:
"O elemento $name não é filho de ${element.name}"

① A função agora retorna um Result<String>.

② No caso de uma exceção na formatação do resultado, uma mensagem de erro ex-


plícita é criada.

③ Se o valor retornado for nulo, ele será substituído por uma mensagem de erro
explícita.

Agora, a maioria dos erros potenciais são tratados de forma funcional, mas nem
todos os erros podem ser tratados funcionalmente. Como eu disse anteriormente,
as exceções que são lançadas pelo efeito passado para o readXmlFile méto-
donão pode ser tratado desta forma. Essas são exceções lançadas pelo programa
retornado pela função. Quando a função retorna o programa, ele ainda não foi
executado. Essas exceções devem ser capturadas durante a execução do programa
resultante, porexemplo:

fun main(args: Array<String>) {


val program = readXmlFile(::getXmlFilePath,
::getRootElementName,
::processElement,
::lista de processos)
tentar {
programa()
} catch (e: Exceção) {
println("Ocorreu uma exceção: ${e.message}")
}
}

Você encontrará o exemplo completo no código que acompanha este livro em


http://github.com/pysaumont/fpinkotlin .

14.4.7 Passo 7: Melhorias adicionais ao código anteriormente imperativo

Pode-se objetar que a processElement funçãofecha sobre as referências for-


mat e elementNames , que podem não parecer funcionais. (Se você não se lembra
do que significa encerramento, consulte o capítulo 3.) Na verdade, esse não é um
problema real no exemplo porque são constantes. Mas na vida real, eles provavel-
mente não seriam constantes.

A solução para este problema é fácil. Esses fechamentos são argumentos implíci-
tos adicionais da processElement função. A indicação do problema é que, ao
contrárioa definição da função, esses dois valores devem fazer parte do programa
cliente, ou seja, a main função.

Você poderia colocar a processElement funçãona main funçãotambém (como


uma função local), mas isso o tornaria não reutilizável. A solução é usar um argu-
mento explícito com uma val função curriedconforme explicado no capítulo 3:

val processElement: (List<String>) -> (String) -> (Elemento) ->


Result<String> = { elementNames ->
{ formato ->
{ elemento ->
tentar {
Result(String.format(format,
*elementNames.map { getChildText(element, it) }
.toArrayList()
.toArray()))
} catch (e: Exceção) {
Result.failure("Exceção ao formatar elemento. " +
"Causa provável é um nome de elemento ausente em" +
" lista de elementos $elementNames")
}
}
}
}

Agora, você pode chamar a readXmlFile funçãocom uma versão parcialmente


aplicada da processElement função. Os dois valores format e elementNa-
mes são específicos para sua implementação de cliente, embora a processEle-
ment função ainda seja genérica e não feche mais sobre esses valores. Você pode
colocar as funções processElement e getChildText genéricas no
ReadXmlFile.kt arquivo, e você pode definir o processList , getRootEle-
mentName , e getXmlFilePath como local para a main função:

fun main(args: Array<String>) {

fun <A> processList(lista: List<A>) = list.forEach(::println)

// Simulando uma computação que pode falhar.


fun getRootElementName(): ElementName = ElementName("pessoal")

fun getXmlFilePath(): FilePath =


FilePath("/caminho/para/arquivo.xml") // <- ajustar caminho
val format = "Nome: %s\n" +
"\tSobrenome: %s\n" +
"\tE-mail: %s\n" +
"\tSalário: %s"

val elementNames =
List("firstName", "lastName", "email", "salário")

val program = readXmlFile(::getXmlFilePath,


::getRootElementName,
processElement(elementNames)(formato),
::lista de processos)
tentar {
programa()
} catch (e: Exceção) {
println("Ocorreu uma exceção: ${e.message}")
}
}

Você pode aplicar o mesmo processo a todas as suas tarefas de programação. Ao


abstrair todas as subtarefas possíveis em uma função, você tornará seus progra-
mas mais testáveis ​e, como consequência, mais confiáveis. Também poderá reuti-
lizar estas funções noutros programas sem ter de as testar novamente. O Apên-
dice B mostra como aplicar a abstração ateste.

Resumo

Colocar valores no Result contexto é o equivalente funcional de asserções.


Os arquivos de propriedade podem ser lidos de maneira segura usando o Re-
sult contexto.
A leitura de propriedades funcionais o livra de lidar com erros de conversão.
As propriedades podem ser lidas como qualquer tipo, enum , ou coleção de
forma abstrata.
Novas tentativas automáticas podem ser abstraídas em funções.
Os wrappers funcionais podem ser construídos em torno de bibliotecas impe-
rativas herdadas.

1  Leia sobre este bug do Kotlin, problema KT-24055, “Uso incorreto de rótulo com
retorno local causa exceção interna no compilador” em
https://youtrack.jetbrains.com/issue/KT-24055 .

You might also like