Escolar Documentos
Profissional Documentos
Cultura Documentos
Nissocapítulo
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
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"})
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:
println(toon.getOrElse{"Sem dados"})
Agora pense no que pode acontecer quando este código for executado:
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.
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:
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
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
override fun <B> map(f: (A) -> B): Ou<E, B> = Esquerda(valor)
override fun <B> map(f: (A) -> B): Ou<E, B> = Direita(f(valor))
Exercício 7.2
abstract fun <B> flatMap(f: (A) -> Qualquer<E, B>): Qualquer<E, B>
Solução
substituir fun <B> flatMap(f: (A) -> Ou<E, B>): Ou<E, B> =
Esquerda(valor)
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
Dica
Solução
A orElse função map retornará uma função constante this e chamará getO-
rElse o resultado:
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.
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.
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 {
⑦ Se uma falha for construída com uma mensagem, ela será agrupada em uma Run-
timeException (mais especificamente, a subclasse IllegalStateException).
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:
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
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:
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:
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?
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 .
objeto complementar {
operador divertido invocar(primeiroNome: String, ③
últimoNome: String) =
Toon(primeiroNome, sobrenome, ④
Result.failure("$firstName $lastName não tem e-mail"))
① Construtor é privado.
④ Se nenhum e-mail for fornecido, um Result.Failure será usado como valor padrão.
println(desenho)
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)
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)
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 .
interno
class Failure<out A>(exceção de valor privado: RuntimeException):
Resultado<A>() {
objeto complementar {
① 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.
① A função get agora retorna Result.Empty se a chave não for encontrada no mapa.
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))
}
}
println(desenho)
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.
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.
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:
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:
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:
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
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
Dica
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:
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:
Exercício 7.8
Dica
Você tem que fazer uma escolha sobre o que retornar em cada caso.
Solução
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.
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.
Exercício 7.9
Dica
Solução
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:
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:
Solução
// 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()
Exercício 7.11
Dica
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:
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:
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.
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
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:
Solução
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
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
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:
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:
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 :
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:
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:
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 :
fun computa(p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) =
Resultado(p1 + p2 + p3 + p4 + p5)
Resumo