Você está na página 1de 48

7Tratamento de erros e exceções

Nissocapítulo

Segurando informações de erro com o Either tipo


Manipulando erros com o Result tipo tendencioso
Acessando e manipulando Result dados
Funções de elevação para operar Result

No capítulo 6, você aprendeu como lidar com dados opcionais sem precisar mani-
pular null referências usando o Option tipo de dados. Como você viu, esse tipo
de dados é perfeito para lidar com a ausência de dados quando isso não é resul-
tado de um erro. Mas não é uma maneira eficiente de lidar com erros porque, em-
bora permita relatar claramente a ausência de dados, ele engole a causa da ausên-
cia. Todos os dados ausentes são tratados da mesma forma e cabe ao chamador
tentar descobrir o que aconteceu. Muitas vezes, isso é impossível.

Neste capítulo, você trabalhará com uma variedade de exercícios que ensinam
como lidar com erros e exceções em Kotlin. Uma habilidade que você aprenderá é
como representar a ausência de dados devido a um erro, o que o Option tipo não
permite. nós vamos primeiroolhar para os problemas que precisam ser resolvidos
e, em seguida, vamos explorar o Either tipoe o Result tipo:
o Either tipoé útil para funções que podem retornar valores de dois tipos
diferentes.
o Result tipoé útil quando você precisa de um tipo que represente dados ou
um erro.

Após algumas discussões e exercícios usando os tipos Either e , você verá como
lidar e aplicar efeitos avançados e também verá uma composição avançada. Re-
sult Result Result

7.1 Os problemas com dados ausentes

A maioriado tempo, a ausência de dados é resultado de um erro, seja nos dados de


entrada ou no cálculo. São dois casos diferentes, mas terminam com o mesmo re-
sultado: os dados estão ausentes e deveriam estar presentes.

Na programação tradicional, quando uma função ou um método recebe um parâ-


metro de objeto, a maioria dos programadores sabe que deve testar esse parâme-
tro para null . Mas o que eles devem fazer se o parâmetro null for muitas vezes
indefinido. Lembre-se do exemplo da listagem 6.2 no capítulo 6:

val goofy = toons.getOption("Goofy").flatMap { it.email }

println(goofy.getOrElse({ "Sem dados" }))

Neste exemplo, a saída “Sem dados” foi obtida porque a Goofy chave não estava
no mapa. Isso pode ser considerado um caso normal. Mas considere este:
val toon = getNome()
.flatMap(toons::getOption)
.flatMap(Toon::email)

println(toon.getOrElse{"Sem dados"})

fun getName(): Option<String> = ???

Se o usuário inserir uma string vazia, o que você deve fazer? Uma solução óbvia
seria validar a entrada e retornar um arquivo Option<String> . Na ausência de
uma string válida, você pode retornar None . Você também sabe que tal operação
pode gerar uma exceção. O programa ficaria assim:

val toon = getNome()


.flatMap(toons::getOption)
.flatMap(Toon::email)

println(toon.getOrElse{"Sem dados"})

fun getName(): Option<String> = tente {


validar(lerLinha())
} catch (e: IOException) {
Opção()
}

fun validar(nome: String?): Option<String> = quando {


nome?.isNotEmpty() ?: false -> Option(nome)
senão -> Opção()
}

Agora pense no que pode acontecer quando este código for executado:

Tudo corre bem e você recebe um e-mail impresso no console.


Um IOException é lançado e você não obtém dados impressos no console.
O nome digitado pelo usuário não valida e você obtém Sem dados.
O nome valida, mas não é encontrado no mapa. Você não obtém dados.
O nome é encontrado no mapa, mas o toon correspondente não possui e-mail.
Você não obtém dados.

O que você precisa são diferentes mensagens impressas no console para indicar o
que está acontecendo em cada caso. Se você quiser usar os tipos que já conhece,
pode usar a Pair<Option<T>, Option<String>> como tipo de retorno de cada
função, mas isso é um pouco complicado. Pair é um tipo de produto, o que signi-
fica que o número de elementos que podem ser representados por a Pair<T,
U> é o número de possíveis T multiplicado pelo número de possíveis U . Você não
precisa disso porque toda vez que tiver um valor para T , terá None para U .

Da mesma forma, cada vez que U for Some , T será None . O que você precisa é
de umtipo de soma , o que significa um tipo E<T, U> que conterá a T ou a U ,
mas não a T e a U . A razão pela qual é chamado de tipo sum é que o número de
diferentes realizações possíveis é a soma do número de realizações possíveis de
T e U . Isso é o oposto de umtipo de produto , como a Pair<T, U> , para o qual o
número de realizações possíveis é o produto do número de possíveis T pelo nú-
mero de possíveis U .
7.2 O tipo

Paralidar com o caso em que uma função pode retornar valores de dois tipos dife-
rentes, como um representando dados e outro representando um erro, você usará
um tipo especial: o Either tipo. Projetar um tipo que pode conter um A ou um
B é fácil. Você precisa modificar um pouco o Option tipo, alterando o None tipo
para que ele contenha um valor. Você também mudará os nomes. As duas subclas-
ses privadas do Either tipo serão chamadas Left e Right conforme mostrado
na listagem a seguir.

Listagem 7.1 O Either tipo

classe selada Ou <fora A, fora B> {

interno
class Left<out A, out B>(private val value: A): Ou<A, B>() {
override fun toString(): String = "Esquerda($valor)"
}

interno
class Right<out A, out B>(private val value: B): Ou<A, B>() {
override fun toString(): String = "Right($value)"
}

objeto complementar {
fun <A, B> esquerda(valor: A): Ou<A, B> = Esquerda(valor)
fun <A, B> direita(valor: B): Ou<A, B> = Direita(valor)
}
}
Agora você pode usar facilmente Either em vez de Option para representar va-
lores que podem estar ausentes devido a erros. Você precisa parametrizar
Either com o tipo de seus dados e o tipo do erro.

Por convenção, você usará a Right subclasse para representar o sucesso (o que é
correto) e a Left subclasse para representar o erro. Mas você não chamará a sub-
classe Wrong porque o Either tipo pode ser usado para armazenar dados que
são representados por um tipo ou outro, sendo ambos válidos.

Você deve escolher qual tipo representará o erro. Você pode optar String por
carregar uma mensagem de erro ou pode preferir usar algum tipo de arquivo
Exception . Por exemplo, a max funçãoretornando o valor máximo da lista que
você definiu no capítulo 6 poderia ser modificado da seguinte forma:

fun <A: Comparável<A>> max(lista: Lista<A>): Ou<String, A> = when(lista) {


é List.Nil -> Tanto.left("max chamado em uma lista vazia")
é List.Cons -> Tanto.right(list.foldLeft(list.head) { x -> { y ->
if (x.compareTo(y) == 0) x else y
}
})
}

Para que o Either tipo seja útil, você precisa de uma maneira de compô-lo. A
composição mais simples é consigo mesma. O resultado de uma função que re-
torna um Either pode precisar ser usado como entrada de outra função que re-
torna outro Either . Para compor funções que retornam Either , você precisa
definir as mesmas funções que definiu na Option classe.
Exercício 7.1

Defina uma map função para transformar um Either<E, A> em um Either<E,


B> , dada uma função de A para B . A assinatura da map função é a seguinte:

diversão abstrata <B> mapa(f: (A) -> B): Ou<E, B>

Dica

Os nomes dos parâmetros E e A são escolhidos para deixar claro qual lado você
deve mapear, E representando erro. Mas seria possível definir duas map funções
(chamá-las mapLeft e mapRight ) para mapear um ou outro lado de uma
Either instância. Você está desenvolvendo uma versão tendenciosa Either que
será mapeável apenas de um lado.

Solução

A Left implementação é um pouco mais complexa do que a None implementa-


ção de Option porque você precisa construir um novo Either contendo o
mesmo valor (erro) do original:

override fun <B> map(f: (A) -> B): Ou<E, B> = Esquerda(valor)

A Right implementação é exatamente como a de Some :

override fun <B> map(f: (A) -> B): Ou<E, B> = Direita(f(valor))
Exercício 7.2

Definir uma flatMap funçãopara transformar um Either<E, A> em um


Either<E, B> , dada uma função de A para Either<E, B> . A assinatura da
flatMap função é a seguinte:

abstract fun <B> flatMap(f: (A) -> Qualquer<E, B>): Qualquer<E, B>

Solução

A Left implementação é exatamente a mesma da map função:

substituir fun <B> flatMap(f: (A) -> Ou<E, B>): Ou<E, B> =
Esquerda(valor)

A Right implementação é a mesma da Option.flatMap função:

override fun <B> flatMap(f: (A) -> Qualquer<E, B>): Qualquer<E, B> = f(valor)

Observe que o E parâmetro foi tornado invariante. Você não precisará se preocu-
par com a variância porque logo se livrará desse parâmetro.

Exercício 7.3

Definir funções getOrElse e orElse com as seguintes assinaturas:


fun getOrElse(defaultValue: () -> @UnsafeVariance A): A

fun orElse(defaultValue: () -> Ou<E, @UnsafeVariance A>): Ou<E, A>

Dica

Cuidado com a variação!

Solução

Para desativar a verificação de variação, ambas as funções devem ter um tipo de


argumento anotado com @UnsafeVariance . o getOrElse função retorna o va-
lor contido quando this é uma instância Right ou o valor de retorno de uma
chamada para a defaultValue função caso contrário. Na Right subclasse, você
precisará alterar o acesso à value propriedade de private para internal para
permitir o acesso da implementação da getOrElse função da superclasse:

fun getOrElse(defaultValue: () -> @UnsafeVariance A): A = quando (este) {


está certo -> this.value
é Esquerda -> defaultValue()
}

A orElse função map retornará uma função constante this e chamará getO-
rElse o resultado:

fun orElse(defaultValue: () -> Ou<E, @UnsafeVariance A>): Ou<E, A> =


mapa { this }.getOrElse(defaultValue)
a Either turmafunciona bem, mas está longe do ideal. O problema é que você
não sabe o que acontece se nenhum valor estiver disponível. Aqui, você obtém o
valor padrão sem saber se é resultado de um cálculo ou de um erro. Para tratar os
casos de erro corretamente, você precisaria de uma versão tendenciosa de
Either , onde o tipo esquerdo é conhecido. Em vez de usar Either (que, aliás,
tem muitos outros casos de uso interessantes), você pode criar uma versão especi-
alizada usando um tipo fixo conhecido para a Left classe.

A primeira pergunta que você pode fazer é: “Que tipo devo usar?” Obviamente,
dois tipos diferentes vêm à mente: String e RuntimeException . Uma string
pode conter uma mensagem de erro, assim como uma exceção, mas muitas situa-
ções de erro produzem uma exceção. Usar a String como o tipo carregado pelo
Left valor forçará você a ignorar as informações relevantes na exceção e usar
apenas a mensagem incluída. É melhor usar RuntimeException como o
Left valor. E se você tiver apenas uma mensagem, você a envolverá em umexce-
ção.

7.3 O tipo de resultado

o quevocê precisa é um tipo que representa dados ou um erro. Como esse tipo ge-
ralmente representa o resultado de um cálculo que pode ter falhado, você o cha-
mará de Result . É semelhante ao Option tipo, exceto que as subclasses são no-
meadas Success e Failure conforme mostrado na listagem a seguir. Como você
pode ver, esta classe é muito parecida com a Option classecom a exceção arma-
zenada adicional.

Listagem 7.2 A Result classe


importar java.io.Serializable

classe selada Resultado<saída A>: Serializável { ①

interno ②
classe Falha<fora A>(
exceção val interna: RuntimeException): ③
Resultado<A>() {
override fun toString(): String = "Falha(${exception.message})"
}

interno ③
class Success<out A>(valor do val interno: A):
Resultado<A>() {
override fun toString(): String = "Success($value)"
}

objeto complementar {

operador divertido <A> invocar(a: A? = null): Resultado<A>


= quando (a) {
nulo -> Falha(NullPointerException()) ⑥
senão -> Sucesso(a)
}

fun <A> falha(mensagem: String): Resultado<A> =


Falha(IllegalStateException(mensagem)) ⑦

fun <A> falha (exceção: RuntimeException): Resultado<A> =


Falha (exceção) ⑧
fun <A> falha(exceção: Exceção): Resultado<A> =
Falha(IllegalStateException(exception)) ⑨
}
}

① A classe Result leva apenas um parâmetro de tipo, correspondente ao valor de


sucesso.

② Os construtores são internos.

③ A subclasse Failure contém uma RuntimeException.

⑥ Se um resultado for construído com um valor nulo, você obterá imediatamente


uma falha.

⑦ Se uma falha for construída com uma mensagem, ela será agrupada em uma Run-
timeException (mais especificamente, a subclasse IllegalStateException).

⑧ Se construído com um RuntimeException, ele é armazenado como está.

⑨ Se construído com uma exceção verificada, é agrupado em um RuntimeException.

Para compor Result , você precisará das mesmas funções definidas nas classes
Option e Either , com pequenas diferenças.

Exercício 7.4

Defina map , flatMap , getOrElse , e orElse para a Result classe. Para getO-
rElse , você pode definir duas funções: uma que recebe um valor como argu-
mento e outra que recebe uma função que produz o valor padrão. Aqui estão as
assinaturas:

fun <B> map(f: (A) -> B): Resultado<B>


fun <B> flatMap(f: (A) -> Resultado<B>): Resultado<B>
fun <A> getOrElse(defaultValue: A): A
fun <A> orElse(defaultValue: () -> Resultado<A>): Resultado<A>

Dica

Não se esqueça de processar qualquer exceção que possa ser lançada por suas im-
plementações e de cuidar da variação.

Solução

Todas as funções são semelhantes às da Either classe. Aqui estão as implementa-


ções de map e flatMap na Success classe:

override fun <B> map(f: (A) -> B): Resultado<B> = try {


Sucesso(f(valor))
} catch (e: RuntimeException) {
Falha(e)
} catch (e: Exceção) {
Falha(RuntimeException(e))
}

override fun <B> flatMap(f: (A) -> Resultado<B>): Resultado<B> = tente {


f(valor)
} catch (e: RuntimeException) {
Falha(e)
} catch (e: Exceção) {
Falha(RuntimeException(e))
}

E aqui estão as implementações para a Failure classe:

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


Falha (exceção)

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


Falha (exceção)

a getOrElse funçãoé útil quando o valor padrão é um literal porque já foi avali-
ado. Nesse caso, você não precisa usar a avaliação preguiçosa. Para evitar proble-
mas de variância, você deve implementar esta função usando a
@UnsafeVariance anotação:

fun getOrElse(defaultValue: @UnsafeVariance A): A = quando (este) {


é sucesso -> this.value
é Falha -> defaultValue
}

o orElse função é usada quando o valor padrão não é avaliado. Como a avalia-
ção pode gerar uma exceção, convém tratar esse caso da seguinte maneira:

fun orElse(defaultValue: () -> Resultado<@UnsafeVariance A>): Resultado<A> =


quando isso) {
é sucesso -> isso
é falha -> tente {
valor padrão()
} catch (e: RuntimeException) {
Resultado.falha<A>(e)
} catch (e: Exceção) {
Result.failure<A>(RuntimeException(e))
}
}

Se você deve lidar com exceções ou não, depende de quais implementações de


função você usará. Se você usar apenas suas próprias implementações, talvez não
precise capturar exceções se nunca lançar uma. Mas se você quiser usar funções
da biblioteca padrão (o que é provável), você precisa lidar com exceções, apli-
cando o princípio seguro: sempre pegue, nunca jogue. Observe também que você
pode ficar tentado a definir a seguinte função, como fez para Option :

fun getOrElse(defaultValue: () -> A): A

Se você quiser ter certeza de nunca lançar uma exceção, não poderá implementar
esta função. O que você retornaria se a função constante () → A lançar umexce-
ção?

7.4 Padrões de resultados

Comessas funções adicionadas, a Result classepode ser usado para compor com
segurança funções que representam cálculos bem-sucedidos ou que falham. Isso é
importante porque Result tipos semelhantes geralmente são considerados con-
têineres que podem ou não conter um valor. Esta descrição está errada.

Result é um contexto computacional para um valor que pode ou não estar pre-
sente. A forma de usar não é recuperando o valor, mas compondo instâncias de
Result usosuas funções específicas. Por exemplo, para usar esta classe, você
pode modificar a ToonMail ilustração anterior. Primeiro você precisa criar uma
get função de extensão especial em Map que retorne um Result.Failure se a
chave não estiver no mapa, conforme mostrado na listagem a seguir. Você pode
chamar esta nova função getResult .

Listagem 7.3 A nova Map.getResult função retornando um Result

fun <K, V> Mapa<K, V>.getResult(chave: K) = quando {


this.containsKey(key) -> Result(this[key]) ①
else -> Result.failure("Chave $key não encontrada no mapa") ②
}

① Se a chave estiver contida no mapa, retorne um Success contendo o objeto


recuperado.

② Caso contrário, retorne uma falha contendo uma mensagem de erro.

Em seguida, modifique a Toon classe conforme mostrado na listagem a seguir.

Listagem 7.4 A Toon classe modificada com a propriedade email modificada


dados
class Toon construtor privado (val firstName: String, ①
val últimoNome: String,
val email: Resultado<String>) { ②

objeto complementar {
operador divertido invocar(primeiroNome: String, ③
últimoNome: String) =
Toon(primeiroNome, sobrenome, ④
Result.failure("$firstName $lastName não tem e-mail"))

operador divertido invocar(primeiroNome: String, ③


últimoNome: String,
e-mail: String) =
Toon(firstName, lastName, Result(email)) ⑥
}
}

① Construtor é privado.

② A propriedade de e-mail agora é um Resultado (que pode ser um Sucesso ou uma


Falha).

③ A função de chamada está sobrecarregada.

④ Se nenhum e-mail for fornecido, um Result.Failure será usado como valor padrão.

⑥ Se o objeto for construído com um e-mail, ele será agrupado em um Result.


Agora modifique o ToonMail programa conforme indicado na listagem a seguir.

Listagem 7.5 O programa modificado usando Result

fun main(args: Array<String>) {

val toons: Map<String, Toon> = mapOf(


"Mickey" para Toon("Mickey", "Mouse", "mickey@disney.com"),
"Minnie" para Toon("Minnie", "Rato"),
"Donald" para Toon("Donald", "Pato", "donald@disney.com"))

val toon = getNome()


.flatMap(toons::getResult) ①
.flatMap(Toon::email)

println(desenho)

fun getName(): Result<String> = try { ②


validar(lerLinha())
} catch (e: IOException) {
Resultado.falha(e)
}

fun validar(nome: String?): Result<String> = quando {


nome?.isNotEmpty() ?: false -> Resultado(nome)
else -> Result.failure("Nome inválido $name")
}
① Os métodos que retornam Result são compostos por flatMap.

② A função getName permite inserir um nome a partir do teclado, resultando em fa-


lha se o nome não for validado ou se uma exceção for lançada.

Você pode modificar a getName funçãopara representar uma exceção sendo lan-
çada retornando um Failure que envolve a exceção.

Observe como as várias operações que retornam a Result são compostas. Você
não precisa acessar o valor contido no Result , o que pode ser uma exceção. a
flatMap funçãoé usado para tal composição. Tente executar este programa com
várias entradas, como estas:

"Mickey"
"Minnie"
"Pateta"
um valor vazio (basta pressionar a tecla Enter)

Aqui está o que o programa imprime em cada caso:

Sucesso (mickey@disney.com)
Falha (Minnie Mouse não tem e-mail)
Falha (Key Goofy não encontrado no mapa)
Falha (nome inválido)

Esse resultado pode parecer bom, mas não é. O problema é que a Minnie (não
tendo e-mail) e o Pateta (não estando no mapa) são relatados como falhas. Podem
ser falhas, mas também podem ser casos normais. Afinal, se não ter e-mail fosse
uma falha, você não teria permitido que uma Toon instância fosse criada sem
um.

Obviamente, isso não é uma falha, mas apenas dados opcionais. O mesmo vale
para o mapa. Pode ser um erro se uma chave não estiver no mapa (assumindo
que deveria estar lá), mas do ponto de vista do mapa, são dados opcionais. Você
pode pensar que isso não é um problema porque você já tem um tipo para isso
(o Option tipo que você desenvolveu no capítulo 6). Mas veja como você compôs
suas funções:

toon = getNome()
.flatMap(toons::getResult)
.flatMap(Toon::email)

Isso só foi possível porque getName , Map.getResult e Toon.email todos retor-


nam um Result . Se Map.getResult e Toon.email fossem retornar Option ,
eles não comporiam mais com getName . Ainda é possível converter um Re-
sult para e de um arquivo Option . Por exemplo, você pode adicionar uma to-
Option função em Result :

diversão abstrata toOption(): Opção<A>

A Success implementação seria

substituir fun toOption(): Option<A> = Option(valor)

A Failure implementação seria


substituir fun toOption(): Option<A> = Option()

Você poderia então usá-lo da seguinte forma:

Opção<String> resultado =
getName().toOption().flatMap(toons::getResult).flatMap(Toon::emmail)

Mas você perderia todos os benefícios de usar Result ! Agora, se uma exceção
for lançada dentro da getName função, ainda está envolvido em a, Failure mas
a exceção é perdida na toOption função e o programa simplesmente imprime

Nenhum

Você pode pensar que deveria ir para o outro lado e converter um Option em um
arquivo Result . Isso funcionaria (embora, em nosso exemplo, você deva chamar
a nova toResult funçãoem ambas as Option instâncias retornadas por
Map.get e Toon.getMail ), mas seria tedioso. Como você geralmente terá que
converter Option para Result , uma maneira muito melhor seria lançar essa
conversão na Result classe. Tudo o que você precisa fazer é criar uma nova sub-
classe correspondente ao None caso. O Some caso não precisa de conversão, ex-
ceto mudar seu nome para Success . A listagem a seguir mostra a nova Re-
sult classe com a nova subclasse chamada Empty .

Listagem 7.6 A nova Result classe manipulando erros e dados opcionais


classe selada Resultado<saída A>: Serializável {

resumo divertido <B> mapa(f: (A) -> B): Resultado<B>

abstract fun <B> flatMap(f: (A) -> Resultado<B>): Resultado<B>

fun getOrElse(defaultValue: @UnsafeVariance A): A = quando (este) {


é Result.Success -> this.value
else -> defaultValue ①
}

fun getOrElse(defaultValue: () -> @UnsafeVariance A): A = quando (este) {


é Result.Success -> this.value
else -> defaultValue() ①
}

fun orElse(defaultValue: () -> Resultado<@UnsafeVariance A>): Resultado<A> =


quando isso) {
é sucesso -> isso
senão -> tente { ①
valor padrão()
} catch (e: RuntimeException) {
Resultado.falha<A>(e)
} catch (e: Exceção) {
Result.failure<A>(RuntimeException(e))
}
}

objeto interno Vazio: Resultado<Nada>() { ④

override fun <B> map(f: (Nothing) -> B): Resultado<B> = Vazio


override fun <B> flatMap(f: (Nothing) -> Result<B>): Result<B> =
Vazio

substituir diversão toString(): String = "Vazio"


}

interno
class Failure<out A>(exceção de valor privado: RuntimeException):
Resultado<A>() {

override fun <B> map(f: (A) -> B): Resultado<B> = Falha(exceção)

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


Falha (exceção)

override fun toString(): String = "Falha(${exception.message})"


}

classe interna Success<out A>(valor de val interno: A): Result<A>() {

override fun <B> map(f: (A) -> B): Resultado<B> = try {


Sucesso(f(valor))
} catch (e: RuntimeException) {
Falha(e)
} catch (e: Exceção) {
Falha(RuntimeException(e))
}

override fun <B> flatMap(f: (A) -> Resultado<B>): Resultado<B> = tente {


f(valor)
} catch (e: RuntimeException) {
Falha(e)
} catch (e: Exceção) {
Falha(RuntimeException(e))
}

override fun toString(): String = "Success($value)"


}

objeto complementar {

operador divertido <A> invocar(a: A? = nulo): Resultado<A> = quando (a) {


null -> Falha(NullPointerException())
senão -> Sucesso(a)
}

operador fun <A> invoca(): Result<A> = Vazio ⑤

fun <A> falha(mensagem: String): Resultado<A> =


Falha(IllegalStateException(mensagem))

fun <A> falha (exceção: RuntimeException): Resultado<A> =


Falha (exceção)

fun <A> falha(exceção: Exceção): Resultado<A> =


Falha(IllegalStateException(exception))
}
}

① As funções getOrElse e orElse foram modificadas para lidar com o caso Empty.
④ Como a instância None em Option, Result contém uma instância singleton de
Empty, parametrizada com Nothing.

⑤ Assim como Option, a função de chamada que não usa nenhum parâmetro re-
torna o singleton Vazio.

Agora você pode modificar novamente seu ToonMail aplicativo conforme mos-
trado nas Listagens 7.7, 7.8 e 7.9.

Listagem 7.7 A getResult função

fun <K, V> Mapa<K, V>.getResult(chave: K) = quando {


this.containsKey(key) -> Result(this[key])
else -> Result.Empty ①
}

① A função get agora retorna Result.Empty se a chave não for encontrada no mapa.

Listagem 7.8 A Toon classe usando Result.Empty para dados opcionais

classe de dados Toon construtor privado (val firstName: String,


val últimoNome: String,
e-mail val: Resultado<String>) {

objeto complementar {
operador divertido invocar(primeiroNome: String,
últimoNome: String) =
Toon(firstName, lastName, Result.Empty) ①
operador divertido invocar(primeiroNome: String,
últimoNome: String,
e-mail: String) =
Toon(firstName, lastName, Result(email))
}
}

① Se você construir a instância sem um e-mail, a propriedade será definida como


Result.Empty.

Listagem 7.9 O aplicativo ToonMail manipulando dados opcionais corretamente

fun main(args: Array<String>) {

val toons: Map<String, Toon> = mapOf(


"Mickey" para Toon("Mickey", "Mouse", "mickey@disney.com"),
"Minnie" para Toon("Minnie", "Rato"),
"Donald" para Toon("Donald", "Pato", "donald@disney.com"))

val toon = getNome()


.flatMap(toons::getResult)
.flatMap(Toon::email)

println(desenho)

fun getName(): Resultado<String> = tente {


validar(lerLinha())
} catch (e: IOException) {
Resultado.falha(e)
}

fun validar(nome: String?): Result<String> = quando {


nome?.isNotEmpty() ?: false -> Resultado(nome)
else -> Result.failure(IOException()) ①
}

① A função de validação é modificada para simular uma IOException quando ne-


nhum nome é inserido.

Seu programa agora imprime os seguintes resultados quando você digita


“Mickey”, “Minnie”, “Goofy” e uma string vazia:

Sucesso (mickey@disney.com)
Vazio
Vazio
Falha(java.io.IOException)

Você pode pensar que algo está faltando porque não consegue distinguir entre os
dois casos vazios diferentes, mas não é o caso. Mensagens de erro não são neces-
sárias para dados opcionais. Se você acha que precisa de uma mensagem, os da-
dos não sãoopcional.

7.5 Tratamento avançado de resultados

Entãoaté agora você viu um uso limitado de Result . Você nunca deve usar Re-
sult para acessar diretamente o valor agrupado (se existir). A maneira que você
usou Result no exemplo anterior corresponde ao caso de uso de composição es-
pecífica mais simples: obter o resultado de um cálculo e usá-lo para a entrada do
próximo cálculo.

Existem casos de uso mais específicos. Você pode usar o valor em Result apenas
se ele corresponder a algum predicado (o que significa alguma condição). Você
também pode usar o caso de falha, para o qual precisaria mapear a falha para ou-
tra coisa ou transformar a falha em um sucesso de exceção. Você também pode
precisar usar vários resultados como entrada para um único cálculo. Você prova-
velmente se beneficiaria de algumas funções auxiliares criadas a Result partir
de cálculos para lidar com o código herdado. Por fim, às vezes você precisará apli-
car efeitos aos resultados.

7.5.1 Aplicação de predicados

Você vaimuitas vezes tem que aplicar um predicado a um arquivo Result . (Um
predicado é uma função que retorna um Boolean .) Isso é algo que pode ser facil-
mente abstraído, de modo que você o escreverá apenas uma vez.

Exercício 7.5

Escreva uma função filter pegando uma condição que é representada por uma
função de A to Boolean e retornando um Result<A> , que será Success ou
Failure , dependendo se a condição vale para o valor envolvido. A assinatura
será esta:

fun filter(p: (A) -> Boolean): Resultado<A>


Crie uma segunda função usando uma condição como seu primeiro argumento e
a String como segundo argumento, usando o argumento de string para o Fai-
lure caso potencial.

Dica

Embora seja possível definir funções abstratas na Result classee para imple-
mentá-los em subclasses, tente não fazer isso. Em vez disso, use uma ou mais fun-
ções definidas anteriormente para criar uma única implementação na Re-
sult classe.

Solução

Você precisa criar uma fun funçãoque usa o valor agrupado como um parâme-
tro, aplica a função de valor do argumento a ele e retorna o mesmo Result se a
condição for válida ou Empty (ou Failure ) caso contrário. Então tudo que você
precisa fazer é flatMap esta função:

fun filter(p: (A) -> Boolean): Result<A> = flatMap {


se (p(isso))
isto
senão
fracasso("Condição não correspondida")
}

fun filter(mensagem: String, p: (A) -> Boolean): Result<A> = flatMap {


se (p(isso))
isto
senão
falha (mensagem)
}

Exercício 7.6

Definir uma exists fun funçãoque recebe uma função de valor de A para Boo-
lean e retorna true se o valor agrupado corresponder à condição ou false não.
Aqui está a assinatura da função:

diversão existe(p: (A) -> Booleano): Booleano

Dica

Mais uma vez, tente não definir uma implementação em cada subclasse. Em vez
disso, crie uma única implementação na classe pai usando as funções que você
tem à sua disposição.

Solução

A solução é map a função para o Result<T> , dando um Result<Boolean> , e


então usar getOrElse com false o valor padrão. Você não precisa usar uma
função constante para produzir preguiçosamente o valor padrão porque é um
literal:

fun existe(p: (A) -> Boolean): Boolean = map(p).getOrElse(false)


Usar exists como nome dessa função pode parecer questionável. Mas é a
mesma função que pode ser aplicada a uma lista, retornando true se pelo menos
um elemento satisfizer a condição. Portanto, faz sentido usar o mesmo nome.

Alguns podem argumentar que essa implementação também funcionaria para


uma forAll funçãoque retorna true se todos os elementos da lista atenderem à
condição. Cabe a você escolher outro nome ou definir uma forAll função na
Result classecom o mesmoimplementação. O ponto importante é entender o que
faz List e Result semelhante e o que fazelesdiferente.

7.6 Falhas de mapeamento

Isso éàs vezes é útil mudar um Failure para outro. Isso é como capturar uma ex-
ceção e relançar algo diferente. O motivo pode ser substituir uma mensagem de
erro por outra mais apropriada, adicionando algumas informações que permitam
ao usuário determinar a causa do problema. Por exemplo, uma mensagem como
“Arquivo de configuração não encontrado” seria muito mais útil se incluísse o ca-
minho que foi pesquisado.

Exercício 7.7

Definir uma mapFailure funçãoque toma a String como argumento e trans-


forma a Failure em outro Failure , usando a string como mensagem de erro.
Se Result for Empty ou Success , esta função não deve fazer nada.

Dica

Defina uma função abstrata na classe pai.


Solução

Aqui está a função abstrata na classe pai:

abstract fun mapFailure(message: String): Resultado<A>

As implementações Empty e Success retornam this . Aqui está a implementa-


ção em Empty :

override fun mapFailure(message: String): Result<Nothing> = this

E aqui está a implementação em Success :

override fun mapFailure(message: String): Resultado<A> = this

A Failure implementação agrupa a exceção existente em uma nova criada com


a mensagem fornecida e retorna um novo Failure :

override fun mapFailure(message: String): Resultado<A> =


Falha(RuntimeException(mensagem, exceção))

Você pode escolher RuntimeException como o tipo de exceção ou um subtipo


personalizado mais específico de uma exceção de tempo de execução. Outra fun-
ção útil seria aquela que mapeia an Empty para a Failure , dado um String
mensagem.
7.7 Adicionando funções de fábrica

você temvisto como você pode criar Success e Failure de um valor. Alguns ou-
tros casos de uso são tão frequentes que merecem ser abstraídos em funções su-
plementares de fábrica. Para adaptar bibliotecas legadas, você provavelmente cri-
ará com frequência Result a partir de um valor que poderia ser null , e deseja
fornecer uma mensagem de erro específica para esse caso. Para fazer isso, você
pode usar uma função no objeto complementar com a seguinte assinatura:

operador divertido <A> invocar(a: A? = nulo, mensagem: String): Resultado<A>

Uma função criando a Result partir de uma função de A para Boolean e uma
instância de A também pode ser útil:

operador divertido <A> invocar(a: A? = nulo, p: (A) -> Booleano): Resultado<A>


operador divertido <A> invocar(a: A? =
null, message: String, p: (A) -> Boolean): Result<A>

Exercício 7.8

Implemente as invoke funções anteriores.

Dica

Você tem que fazer uma escolha sobre o que retornar em cada caso.
Solução

Este exercício não apresenta dificuldades. Aqui estão algumas implementações


possíveis com base na opção de retornar Empty quando nenhuma mensagem de
erro é usada e Failure caso contrário:

operador divertido <A> invocar(a: A? = nulo, mensagem: String): Resultado<A> =


quando um) {
null -> Falha(NullPointerException(mensagem))
senão -> Sucesso(a)
}

operador divertido <A> invocar(a: A? = nulo, p: (A) -> Booleano): Resultado<A> =


quando um) {
null -> Falha(NullPointerException())
senão -> quando {
p(a) -> Sucesso(a)
senão -> Vazio
}
}

operador divertido <A> invocar(a: A? = null,


mensagem: String,
p: (A) -> Booleano): Resultado<A> =
quando um) {
null -> Falha(NullPointerException())
senão -> quando {
p(a) -> Sucesso(a)
else -> Falha(IllegalArgumentException(
"O argumento $a não corresponde à condição: $mensagem"))
}
}

Observe que você não precisa converter o argumento a para o tipo A (em vez de
A? ) antes de aplicar o predicado. Kotlin sabe que não pode ser null da verifica-
ção anterior no primeiro when cláusula.

7.8 Aplicação de efeitos

Entãoaté agora você não aplicou nenhum efeito aos valores agrupados em Re-
sult , além de obter esse valor (através getOrElse ). Isso não é satisfatório por-
que destrói a vantagem de usar Result . Por outro lado, você ainda não apren-
deu o necessáriotécnicas para aplicar efeitos de forma segura. Os efeitos podem
ser qualquer coisa que modifique algo no mundo externo, como gravar no con-
sole, em um arquivo, em um banco de dados ou em um campo em um compo-
nente mutável ou enviar uma mensagem localmente ou por uma rede.

IMPORTANTE A técnica que vou mostrar agora não é segura, então ela só deve
ser usada depois que todo o cálculo for feito. Os efeitos devem ser aplicados em
uma parte especificamente delimitada do seu código e nenhum cálculo adicional
deve ser executado nos valores depois que você aplicar os efeitos. Se você preci-
sar aplicar efeitos de forma segura e depois continuar alguns cálculos, terá que es-
perar até o capítulo 12 para aprender as técnicas necessárias.

Aplicar um efeito na programação padrão consistiria em extrair o valor (se pre-


sente) do Result e executar alguma ação com ele. Para uma programação mais
segura, você faz o contrário: você passa o efeito para o Result para que seja apli-
cado ao valor contido (se presente).
Para representar um efeito em Kotlin, você usa uma função que não retorna nada
e executa o efeito pretendido. Isso não deveria ser chamado de função, mas é as-
sim no Kotlin. Em Java, isso seria chamado de arquivo Consumer . O tipo de re-
torno dessa função em Kotlin é Unit .

Exercício 7.9

Definir uma forEach funçãoque toma um efeito como parâmetro e o aplica ao


valor agrupado.

Dica

Definir uma função abstrata na Result classecom uma implementação em cada


subclasse.

Solução

Aqui está a declaração da função abstrata em Result :

diversão abstrata paraCada(efeito: (A) -> Unidade)

Observe que ambos forEach e o effect parâmetro retornam Unit . Mas para
uma fun funçãoretornando Unit , você pode omitir o tipo de retorno. As Fai-
lure implementações não fazem nada:

substituir fun forEach(efeito: (A) -> Unidade) {}


A Empty implementação é semelhante, mas com uma assinatura ligeiramente
diferente:

substituir fun forEach(efeito: (Nada) -> Unidade) {}

A Success implementação é simples. Você precisa aplicar o efeito ao valor:

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


efeito(valor)
}

esta forEach funçãoseria perfeito para a Option classevocê criou no capítulo 6.


Mas esse não é o caso de Result . Geralmente, você deseja realizar ações especi-
ais em caso de falha.

Exercício 7.10

Defina a forEachOrElse funçãopara lidar com este caso de uso. Terá que lidar
com ambos Failure os Empty casos. Aqui está sua assinatura na Result classe:

diversão abstrata forEachOrElse(onSuccess: (A) -> Unidade,


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

Solução

Todas as três implementações executam a função correspondente:


// Sucesso
substitua a diversão porEachOrElse(onSuccess: (A) -> Unidade,
onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) = onSuccess(valor)

// Falha
substitua a diversão porEachOrElse(onSuccess: (A) -> Unidade,
onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) = onFailure(exception)

// Vazio
substitua a diversão porEachOrElse(onSuccess: (Nothing) -> Unit,
onFailure: (RuntimeException) -> Unidade,
onEmpty: () -> Unidade) = onEmpty()

Observe que você não lança nenhuma exceção. É responsabilidade do chamador


lidar com a exceção. Se o programador quiser lançá-lo, ele deverá fornecer { th-
row it } o segundo argumento.

Exercício 7.11

a forEachOrElse funçãofunciona bem, mas não é o ideal. Na verdade, o forEa-


ch tem o mesmo efeito de forEachOrElse usar argumentos específicos, por-
tanto, o código é duplicado. Você pode consertar isso?

Dica

Todos os argumentos devem ser opcionais.


Solução

A solução consiste em usar valores padrão para os três argumentos. Aqui está a
nova declaração abstrata na Result classe pai:

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


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

A nova função pode ser renomeada para substituir a forEach função original. As
implementações nas subclasses não mudam. Agora você pode chamar forEa-
ch fornecendo um efeito para Success e Empty ignorando Failure (por exem-
plo) usando argumentos nomeados:

val resultado: Result<Int> = if (z % 2 == 0) Result(z) else Result()


result.forEach({ println("$é par") }, onEmpty =
{ println("Este é estranho") })

Observe que você só precisa de argumentos nomeados depois de pular um. Aqui,
você não precisa de um nome para o primeiro argumento. Você só precisa no-
mear um argumento que não esteja na posição pretendida. O terceiro argumento
da função é onEmpty , mas vem em segundo lugar, então precisa ser nomeado.

Um caso de uso frequente para a forEach funçãoé o seguinte. Este exemplo usa
uma log função hipotética no nível do pacote:
val resultado = getComputation()

result.forEach(::println, ::log)

Lembre-se de que essas funções não são realmente funções, mas são maneiras
boas e simples de usar Result . Mais sobre isso emcapítulo 12.

7.9 Composição avançada de resultados

Usarcasos para Result são mais ou menos os mesmos que para Option . No ca-
pítulo 6, você definiu uma lift funçãopara compor Option instâncias transfor-
mando uma função de A para B em uma função de Option<A> para
Option<B> . Você pode fazer o mesmo para Result , no qual trabalhará nesta se-
ção por meio de uma série de exercícios.

Exercício 7.12

Escreva uma lift funçãopara Result . Coloque-o no nível do pacote com a se-
guinte assinatura:

fun <A, B> lift(f: (A) -> B): (Resultado<A>) -> Resultado<B>

Solução

Aqui está a solução simples:

fun <A, B> lift(f: (A) -> B): (Resultado<A>) -> Resultado<B> = { it.map(f) }
Ao contrário Option de , não há necessidade de capturar exceções que podem ser
lançadas pela f função porque elas já são tratadas por map .

Exercício 7.13

Defina lift2 para levantar uma função de A para ( B para C ) e lift3 para fun-
ções de A para ( B para ( C para D )) com as seguintes assinaturas:

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


(Resultado<A>) -> (Resultado<B>) -> Resultado<C>
divertido <A, B, C, D> elevador3(f: (A) -> (B) -> (C) -> D):
(Resultado<A>) -> (Resultado<B>) -> (Resultado<C>) -> Resultado<D>

Solução

Aqui estão as soluções:

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


(Resultado<A>) -> (Resultado<B>) -> Resultado<C> =
{a->
{b->
a.map(f).flatMap { b.map(it)}
}
}

divertido <A, B, C, D> elevador3(f: (A) -> (B) -> (C) -> D):
(Resultado<A>) -> (Resultado<B>) -> (Resultado<C>) -> Resultado<D> =
{a->
{b->
{c->
a.map(f).flatMap { b.map(it)}.flatMap { c.map(it) }
}
}
}

Eu estou supondo que você pode ver o padrão. Você pode definir lift qualquer
número de parâmetros dessa maneira.

Exercício 7.14

No capítulo 6, você definiu uma map2 função, tomando como argumentos an


Option<A> , an Option<B> , e uma função de A to ( B to C ) e retornando um
Option<C> . Defina uma map2 função para Result .

Dica

Não use a função que você definiu para Option . Em vez disso, use o lift2 fun-
ção que você definiu no exercício 7.13.

Solução

A solução que você definiu Option foi

fun <A, B, C> map2(oa: Opção<A>,


ob: Opção<B>,
f: (A) -> (B) -> C): Opção<C> =
oa.flatMap { a -> ob.map { b -> f(a)(b) } }
Este é o mesmo padrão que você usou para lift2 . A map2 função é
simplesmente

fun <A, B, C> map2(a: Resultado<A>,


b: Resultado<B>,
f: (A) -> (B) -> C): Resultado<C> = elevador2(f)(a)(b)

Um caso de uso comum para tais funções é chamar funções ou construtores com
argumentos do tipo Result retornados por outras funções. Veja o ToonMai-
l exemplo anterior. Para preencher o Toon mapa, você pode construir toons pe-
dindo ao usuário para inserir o nome, sobrenome e e-mail no console, usando as
seguintes funções:

fun getFirstName(): Result<String> = Result("Mickey")

fun getLastName(): Result<String> = Result("Mouse")

fun getMail(): Result<String> = Result("mickey@disney.com")

A implementação real será diferente, mas você ainda precisa aprender como ob-
ter entrada com segurança do console. Por enquanto, você usará essas implemen-
tações fictícias. Usando essas implementações, você pode criar um Toon da se-
guinte forma:

var createPerson: (String) -> (String) -> (String) -> Toon =


{ x -> { y -> { z -> Toon(x, y, z) } } }
val toon = lift3(createPerson)(getFirstName())(getLastName())(getMail())

Mas você está atingindo os limites da abstração. Você pode ter que chamar fun-
ções ou construtores com mais de três argumentos. Nesse caso, você pode usar o
seguinte padrão, que às vezes échamada compreensão :

val toon = getPrimeiroNome()


.flatMap { firstName ->
getLastName()
.flatMap { lastName ->
receber mensagens()
.map { mail -> Toon(firstName, lastName, mail) }
}
}

O padrão de compreensão tem duas vantagens:

Você pode usar qualquer número de argumentos.


Você não precisa definir uma função.

Observe que você poderia usar lift3 sem definir a função separadamente, mas
teria que especificar os tipos por causa da capacidade limitada de inferência de
tipo do Kotlin:

val toon2 = elevador3 { x: String ->


{ y: String ->
{ z: String ->
Desenho(x, y, z)
}
}
}(getFirstName())(getLastName())(getMail())

Algumas linguagens têm açúcar sintático para tais construções, aproximada-


mente equivalente a isto:

por {
primeiroNome em getPrimeiroNome(),
lastName em getLastName (),
e-mail em getMail()
} return new Toon(firstName, lastName, mail)

Kotlin não tem esse tipo de açúcar sintático, mas é fácil passar sem ele. Observe
que as chamadas para flatMap ou map estão aninhados. Comece com uma cha-
mada para a primeira função (ou comece a partir de uma Result instância),
flatMap cada nova chamada e termine mapeando a chamada para o construtor
ou função que você pretende usar. Por exemplo, para chamar uma função com
cinco parâmetros quando você tem apenas cinco Result instâncias, use a se-
guinte abordagem:

val resultado1 = Resultado(1)


val resultado2 = Resultado(2)
val resultado3 = Resultado(3)
val resultado4 = Resultado(4)
val resultado5 = Resultado(5)

fun computa(p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) =
p1 + p2 + p3 + p4 + p5
val resultado = resultado1.flatMap { p1: Int ->
result2.flatMap { p2 ->
result3.flatMap { p3 ->
result4.flatMap { p4 ->
result5.map { p5 ->
calcular(p1, p2, p3, p4, p5)
}
}
}
}
}

Este exemplo é um pouco artificial, mas mostra como o padrão pode ser esten-
dido. Observe que não é inerente ao padrão que a última chamada (a mais pro-
fundamente aninhada) seja em map vez de flatMap . Isso porque a última fun-
ção ( compute ) retorna um valor bruto. Se retornasse um Result , você teria
que usar flatMap em vez de map :

val resultado1 = Resultado(1)


val resultado2 = Resultado(2)
val resultado3 = Resultado(3)
val resultado4 = Resultado(4)
val resultado5 = Resultado(5)

fun computa(p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) =
Resultado(p1 + p2 + p3 + p4 + p5)

val resultado = resultado1.flatMap { p1: Int ->


result2.flatMap { p2 ->
result3.flatMap { p3 ->
result4.flatMap { p4 ->
result5.flatMap { p5 ->
calcular(p1, p2, p3, p4, p5)
}
}
}
}
}

Mas como esta última função é frequentemente um construtor, e os construtores


sempre retornam valores brutos, você frequentemente se verá usando map como
últimofunçãoligar.

Resumo

É necessário representar a ausência de dados devido a um erro. Como o Op-


tion tipo não permite isso, você precisa do Result tipo. Você também pode
usar o Either tipo para representar dados de um tipo ( Right ) ou outro (
Left ).
Either pode ser mapeado ou mapeado de forma plana como Option , mas
pode estar em ambos os lados ( Right ou Left ).
Either pode ser enviesado fazendo com que um lado ( Left ) represente
sempre o mesmo tipo ( RuntimeException ). Esse tipo tendencioso é cha-
mado Result . O sucesso é representado por um Success subtipo e o fra-
casso por um Failure subtipo.
Uma maneira de usar o Result tipo é obter o valor agrupado se estiver pre-
sente ou usar um valor padrão fornecido caso contrário. O valor padrão, se
não for literal, deve ser avaliado lentamente.
Compor Option (representar dados opcionais) com Result (representar da-
dos ou um erro) é tedioso. Esse caso de uso é facilitado pela adição de um
Empty subtipo a Result , tornando o Option tipo inútil.
Você pode mapear falhas, se necessário: por exemplo, para tornar as mensa-
gens de erro mais explícitas.
Várias funções de fábrica simplificam a Result criação de várias situações,
como o uso de dados anuláveis ​ou dados condicionais, que são representados
por dados e uma condição que deve ser atendida.
Você pode aplicar efeitos Result através da forEach função. Esta função per-
mite aplicar diferentes efeitos a Success , Failure e Empty .
Você pode levantar funções de A para B (usando a lift função) para operar
de Result<A> para Result<B> . Você pode elevar funções de A para ( B para
C ) (através da lift2 função) para uma função de Result<A> para (
Result<B> para Result<C> ).
Você pode usar o padrão de compreensão para compor qualquer número de
Result dados.

Você também pode gostar