Escolar Documentos
Profissional Documentos
Cultura Documentos
6 Dealing With Optional Data - The Joy of Kotlin
6 Dealing With Optional Data - The Joy of Kotlin
Nissocapítulo
A maioria dos dados é representada por uma referência que aponta para eles,
portanto, a maneira mais óbvia de representar a ausência de dados é usar um
ponteiro para nada. Isso é o que chamamos deponteiro nulo . Em Kotlin, uma refe-
rência é um ponteiro para um valor.
Na maioria das linguagens de programação, as referências podem ser alteradas
para apontar para um novo valor. Por esse motivo, o termo variável é frequente-
mente usado como substituto de referência . A variável de palavra não é um
grande problema quando todas as referências podem ser reatribuídas posterior-
mente a um novo valor. Nesses casos, é normal usar o termo variável como substi-
tuto de referência . Mas isso pode ser diferente quando você usa referências que
não podem ser reatribuídas. Nesse caso, você acaba com dois tipos de variáveis:
as que podem variar e as que não podem. É então mais seguro usar o termo refe-
rência .
Algumas referências podem ser criadas null e depois alteradas para apontar
para valores. Eles podem até ser alterados novamente para apontar null se os
dados forem removidos. Nesses casos, é seguro chamá-los de variables , ou seja ,
referências de variáveis .
Por outro lado, algumas referências não podem ser criadas sem que apontem
para um valor e, uma vez feito isso, não podem ser feitas para apontar para um
valor diferente. Tais referências às vezes são chamadasconstantes . Em Java, por
exemplo, as referências geralmente são variáveis, a menos que sejam declaradas
final , o que as torna constantes. Em Kotlin, as referências variáveis são declara-
das com a palavra-chave var , e as não variáveis, também chamadasreferências
imutáveis , são declaradas como val .
A princípio, você pode pensar que os dados opcionais só podem ser referenciados
com var . Qual seria o sentido de criar uma referência a uma ausência de dados
se não para eventualmente mudar isso para uma referência a alguns dados? Mas
como você representaria a ausência de dados? A maneira mais comum é usar o
que chamamos dereferência nula . Uma referência nula é uma referência que não
aponta para nada, até que alguns dados sejam atribuídos a ela.
Neste capítulo, você aprenderá como lidar com dados opcionais — valores ausen-
tes que não são resultado de um erro. Começo discutindo problemas com o pon-
teiro nulo. Depois disso, discuto como Kotlin lida com referências nulas e as alter-
nativas de Kotlin para referências nulas. No restante do capítulo, descrevo como
criar e usar o Option tipo como uma solução para trabalhar com dados opcio-
nais. Você pode usar o Option tipo para compor funções mesmo quando os dados
estiverem ausentes.
Embora agora deva ser de conhecimento comum evitar a referência nula, isso
está longe de ser o caso. A biblioteca padrão Java, que também é usada em progra-
mas Kotlin, contém métodos e construtores que usam parâmetros opcionais que
devem ser definidos null se os parâmetros estiverem ausentes. Veja, por exem-
plo, a java.net.Socket classe. Essa classe define o seguinte construtor:
Esse tipo de valor às vezes é chamado devalor sentinela . Não é usado para o valor
em si (não significa porta 0), mas é usado para especificar a ausência de uma indi-
cação de porta. Você pode encontrar muitos outros exemplos de tratamento da au-
sência de dados na biblioteca padrão Java.
Você sabe que nunca deve usar uma referência sem verificar se é null ou
não. (Você faz isso para cada parâmetro de objeto recebido por uma função,
certo?)
Você sabe que nunca deve obter um valor de um mapa sem primeiro testar se
o mapa contém a chave correspondente.
Você sabe que nunca deve tentar obter um elemento de uma lista sem verificar
primeiro se a lista não está vazia e se possui elementos suficientes se você esti-
ver acessando o elemento por meio de seu índice.
Sempre há truques para lidar com dados opcionais. Uma das mais conhecidas e
mais utilizadas é a lista. Quando uma função deve retornar um valor ou nada, al-
guns programadores usam a lista como valor de retorno. A lista pode conter 0 ou
1 elemento. Embora isso funcione muito bem, tem váriosdesvantagens:
DentroNo capítulo 2, você aprendeu como o Kotlin lida com referências nulas. Ti-
pos normais em Kotlin não podem ser nulos. Se uma referência recebe esse tipo
de tipo, ela nunca pode ser nula, seja a val (constante) ou a var (variável). Um
nulo val faria pouco sentido. Um null var , por outro lado, pode parecer útil. Às
vezes, você pode querer declarar uma referência de um determinado tipo sem ter
nenhum valor atribuído a ela. Mas você ainda deve declarar a referência em al-
gum ponto para colocá-la no escopo correto. Considere o seguinte exemplo:
while(enumeração.hasNext()) {
resultado = processo(resultado, enumeração.próximo())
}
usar (resultado)
Mas isso causaria um erro se a enumeração não tivesse nenhum elemento. A solu-
ção comum é declarar a result referência como null :
var resultado = nulo
while(enumeração.hasNext()) {
resultado = processo(resultado, enumeração.próximo())
}
usar (resultado)
Agora, a process funçãodeve lidar com o caso quando result é null . (No pri-
meiro caso, ele foi sobrecarregado com uma versão que aceita um único argu-
mento.) Um null resultado agora é um negócio nulo, o que significa que é a pri-
meira iteração. Mas Kotlin não permitirá que você faça isso.
Kotlin usa referências nulas, mas força você a informar ao compilador que sabe
que a referência pode ser nula. Isso não vai impedir NullPointException , mas
obriga você a aceitar a responsabilidade de fazê-lo. É como um aviso: se você usar
a null referência, estará por sua conta. O compilador do Kotlin pode ajudá-lo de
várias maneiras, o que é ótimo para programadores que usam referências mutá-
veis. Os programadores que evitam mutações, por outro lado, não usam referên-
cias nulas. Mas a maneira como o Kotlin representa os tipos anuláveis ainda é
uma grande ajuda para uma programação segura. Com o Kotlin, os programado-
res podem garantir que nunca haverá referências nulas em seu código. (Consulte
o capítulo 2 para obter uma breve descrição das diferenças entre tipos anuláveis e
não anuláveis em Kotlin.)
a mean função é um exemplo de função parcial, como você viu no capítulo 3. Ela é
definida para todas as listas de números, exceto para a lista vazia. Como você
deve lidar com o caso de lista vazia? Uma possibilidade é retornar um valor senti-
nela. Que valor você poderia escolher? Como o tipo é Double , você pode usar um
valor definido na Double classe:
As exceções geralmente são usadas para resultados incorretos, mas aqui não
há erro. Não há resultado e isso ocorre porque não há dados de entrada! Ou
você deve considerar chamar a função com uma lista vazia de bug?
Que exceção você deve lançar? Um personalizado? Ou um padrão?
A função não é mais uma função pura e não pode mais ser composta com ou-
tras funções. Para compô-lo, você precisa recorrer a estruturas de controle
como try..catch , que nada mais são do que uma forma moderna de goto .
Você também pode retornar null e deixar o chamador lidar com isso:
fun mean(list: List<Int>): Duplo? = quando {
list.isEmpty() -> nulo
else -> list.sum().toDouble() / list.size
}
Ele força o chamador a lidar com o null caso escolhendo um tipo de retorno
anulável.
Ele não travará no boxe porque o Kotlin não possui tipos primitivos, portanto,
não há boxe (pelo menos no nível do usuário).
Oferece operadores que permitem compor funções retornando um tipo anulá-
vel de forma segura. (Consulte o capítulo 2 para obter detalhes.)
Embora não impeça a propagação do problema, torna o problema menos cru-
cial ao propagar a obrigação de tratá-lo.
Como eu disse anteriormente, a solução Kotlin é melhor do que a ausência de
uma solução com a qual você tenha que lidar em outras linguagens, mas ainda
está longe do ideal. Uma solução melhor seria pedir ao usuário para fornecer um
valor especial que será retornado se nenhum dado estiver disponível. Por exem-
plo, a função a seguir calcula o valor máximo de uma lista. Ele usa a
List.max() função do Kotlin, retornando Int? , mas trata o caso de uma lista
vazia para que você possa fazer sua função retornar Int ao invés de Int? :
Mas isso não vai compilar porque list.max() o tipo de retorno é Int? . (Nunca
será, null mas Kotlin não vê isso.) Você sabe que a solução idiomática Kotlin é
Mas e se você não tiver um valor padrão? Às vezes, um valor padrão será forne-
cido posteriormente. Ou você deseja aplicar um efeito ao resultado se houver um
valor e não fazer nada caso contrário. Veja o exemplo a seguir usando a max fun-
ção anterior:
Mas isso seria incorreto se a lista contivesse apenas 0(s). Você pode usar a forma
tradicional:
Mas agora você tem que lidar com referências nulas novamente. Tem que haver
um melhorsolução!
DentroNo restante deste capítulo, você criará o Option tipo para lidar com dados
opcionais. O Option tipo que você compõe será semelhante ao List tipo. Você a
implementará como uma classe selada abstrata Option , contendo duas subclas-
ses internas que representam a presença e a ausência de dados.
Figura 6.1 Usando um Option tipo para dados opcionais permite compor funções mesmo quando os dados es-
tão ausentes. Sem o Option tipo, as funções de composição não produziriam uma função porque o programa
resultante lançaria potencialmente um arquivo NullPointerException .
objeto complementar {
④ Instâncias de Some são consideradas iguais se seus valores forem iguais, que é o
que você obtém usando uma classe de dados.
Do jeito que está, a Option classe não é útil. Tudo o que você pode fazer é testar
se o resultado está vazio ou imprimi-lo. Você poderia ter adicionado uma função
para retornar o valor, mas esta função teria que lançar uma exceção ou retornar
null quando chamada em None . Retornar null iria anular todo o propósito de
Option . Lançar exceções é ruim porque não compõe ou, pelo menos, não com-
põe da mesma forma que outros resultados compõem. Se você conhece Java, sabe
que o Optional class em Java tem um get método retornando o valor se pre-
sente ou lançando uma exceção caso contrário.
Aqui está o que Brian Goetz, Java Language Architect da Oracle, diz sobre isso: 3
Anúncio de serviço público: NUNCA ligue para Optional.get a menos que você
possa provar que nunca será nulo; em vez disso, use um dos métodos seguros
como orElse ou ifPresent. Em retrospecto, deveríamos ter chamado get algo
comogetOrElseThrowNoSuchElementException ou algo que deixou muito
mais claro que este era um método altamente perigoso que prejudicou todo o
propósito de Optional em primeiro lugar. Lição aprendida.
A realidade é muito mais simples: não deveria haver nenhum get método em
Java Optional e você também não colocará um na Option classe. Aqui está o
motivo: Option é um contexto computacional para lidar com dados opcionais
com segurança. Dados opcionais são potencialmente inseguros, e é por isso que
você precisa de um contexto seguro para colocá-los. Como consequência, você
nunca deve retirar um valor desse contexto sem primeiro torná-lo seguro.
Muitosfunções que você criou List também serão úteis para arquivos Option .
Mas antes de criar essas funções, vamos começar com alguns Option usos especí-
ficos. Lembre-se do uso de um valor padrão?
Exercício 6.1
Agora você pode definir funções que retornam opções e usam o valor retornado
de forma transparente da seguinte maneira (usando a List classe Kotlin padrão):
Aqui, max1 é igual a 7 (o valor máximo na lista) e max2 é definido como 0 (o valor
padrão). Mas você ainda pode ter um problema. Observe o seguinte exemplo:
Este exemplo não imprime nada e lançará uma exceção diretamente porque Ko-
tlin é uma linguagem estrita. Os parâmetros da função são avaliados antes que a
função seja executada, sejam eles necessários ou não. Isso significa que o getO-
rElse parâmetro da função é avaliado em qualquer caso, seja chamado em a So-
me ou a None . O fato de o parâmetro da função não ser necessário para a Some é
irrelevante. Isso não faz diferença quando o parâmetro é literal, mas faz uma
grande diferença quando é uma chamada de função. A getDefault função será
chamada em qualquer caso, então a primeira linha lançará uma exceção e nada
será exibido. Isso geralmente não é o que você deseja.
Exercício 6.2
Dica
Em vez de usar um valor literal, use uma função sem argumento que retorne o
valor.
Solução
Umfunção importante em List é a map função, que permite aplicar uma função
de A a B para cada elemento de uma lista de A , produzindo uma lista de B . Con-
siderando que an Option é como uma lista contendo no máximo um elemento,
você pode aplicar o mesmo princípio.
Exercício 6.3
Você pode definir uma função abstrata na Option classe com uma implementa-
ção em cada subclasse. Alternativamente, você pode implementar a função
no Option classe. No primeiro caso, esteja ciente do tipo na None classe. A assina-
tura da função abstrata em Option será esta:
Solução
A Some implementação não é muito mais complexa. Tudo o que você precisa fa-
zer é obter o valor, aplicar a função a ele e agrupar o resultado em um novo
Some :
Exercício 6.4
Crie uma flatMap função de instância tomando como argumento uma função de
A to Option<B> e retornando um Option<B> .
Dica
Tente usar algumas das funções que você já definiu ( map e getOrElse ).
Solução
A solução trivial seria definir uma função abstrata na Option classe, retor-
nando None na None classe e retornando Some(f(value)) na Some classe. Esta
é provavelmente a implementação mais eficiente. Mas uma solução mais elegante
é mapear a f função, dando um Option<Option<B>>, e então usar o getOrEl-
se função para extrair o valor ( Option<B> ), fornecendo None como valor
padrão:
Exercício 6.5
Assim como você precisava de uma maneira de mapear uma função que retor-
nasse um Option (conduzindo a flatMap ), você precisaria de uma versão de va-
lores padrão de getOrElse for s. Option Crie o orElse função com a seguinte
assinatura:
Como você pode imaginar pelo nome, não há necessidade de obter o valor para
implementar essa função. É assim que Option é usado principalmente - por meio
da Option composição, em vez de agrupar e obter valores. Uma consequência é
que a mesma implementação funciona para ambas as subclasses. Não se esqueça,
no entanto, de cuidar da variância.
Solução
No capítulo 5, você criou uma filter função para remover de uma lista todos os
elementos que não satisfizeram uma condição expressa na forma de um predi-
cado (ou seja, uma função que retorna um Boolean ). Crie a mesma função para
Option . Aqui está sua assinatura:
Dica
Solução
Observe que é prática comum usar o nome p (para predicado ), que é uma função
que retorna um Boolean .
6.4.4 Casos de uso de opções
Sevocê já conhece a Optional classe Java, você deve ter notado que Optio-
nal contém um isPresent() método que permite testar se o Optional contém
um valor ou não. ( Optional tem uma implementação diferente que não é base-
ada emduas subclasses diferentes.) Você poderia implementar facilmente essa
função em sua Option classe Kotlin, embora a chamasse isSome() porque testa-
ria se o objeto é a Some ou a None . Você também pode chamá-lo isNone() de , o
que pode parecer mais lógico, porque seria o equivalente à
List.isEmpty() função.
Embora a isSome() função às vezes possa ser útil, não é a melhor maneira de
usar a Option classe. Se você fosse testar um Option através de
um isSome() função antes de chamar algum tipo de getOrThrow() para obter o
valor, não seria muito diferente de testar uma referência null antes de desrefe-
renciá-la. A única diferença seria se você esquecesse de testar primeiro: você cor-
reria o risco de ver um IllegalStateException ou NoSuchElementExcepti-
on ouqualquer exceção que você escolheu para a None implementação de ge-
tOrThrow() , em vez de um NullPointerException .
A melhor forma de usar Option é através da composição. Para fazer isso, você
deve criar todas as funções necessárias para todos os casos de uso. Esses casos de
uso correspondem ao que você faria com o valor após testar se não é null . Você
poderia, por exemplo
O primeiro e o terceiro casos de uso já foram possíveis por meio das funções que
você já criou. A aplicação de um efeito pode ser feita de diferentes maneiras, so-
bre as quais você aprenderá no capítulo 12.
Por exemplo, veja como você pode usar a Option classe para alterar a maneira
como usaria um mapa. Na listagem 6.2 , você usará uma função de extensão on
Map para retornar um Option ao consultar uma determinada chave. A imple-
mentação Kotlin padrão de Map.get(key) , que você pode usar como
Map[key] , retorna null se a chave não for encontrada. Você vai usar o getOp-
tion função de extensão para retornar um arquivo Option .
import com.fpinkotlin.optionaldata.exercise06.Option
import com.fpinkotlin.optionaldata.exercise06.getOrElse
objeto complementar {
operador divertido invocar(primeiroNome: String,
últimoNome: String,
e-mail: String? = nulo) =
Toon(firstName, lastName, Option(email))
}
}
fun <K, V> Mapa<K, V>.getOption(chave: K) =
Opção(esta[tecla]) ①
val mickey =
toons.getOption("Mickey").flatMap { it.email } ②
val minnie = toons.getOption("Minnie").flatMap { it.email }
val goofy = toons.getOption("Goofy").flatMap { it.email }
Listagem 6.3 Usando tipos anuláveis em um estilo Kotlin mais idiomático
println(mickey)
println(minnie)
println(pateta)
}
Nesta fase, você pode achar o estilo Kotlin mais conveniente. Mas há um pequeno
problema. Ambos os programas exibem o seguinte resultado:
Some(value=mickey@disney.com)
Nenhum
sem dados
A primeira linha é o e-mail do Mickey. A segunda linha diz “ None ” porque Min-
nie não tem e-mail. A terceira linha diz “ No data ” porque o Pateta não está no
mapa. Claramente, você precisa de uma maneira de distinguir esses dois casos,
mas nem o uso de tipos anuláveis nem a Option classe permitem distinguir entre
os dois. No capítulo 7, você verá como resolver esse problema.
Exercício 6.7
Dica
Para implementar esta função, você deve primeiro criar uma mean função. Se
você tiver problemas para definir a mean função, consulte o capítulo 5 ou use o
seguinte:
Solução
Usar funções de valor não é obrigatório, mas você deve usar funções de valor se
precisar passá-las como argumentos para HOFs. Quando você só precisa aplicá-
los, no entanto, fun as funçõespode ser mais simples de usar. Se você preferir
usar fun as funçõesquando possível, você pode chegar à seguinte solução:
Como você pode ver, fun as funçõessão mais simples de usar porque os tipos são
mais simples. Por esse motivo, você usará fun funçõesem vez de funções de valor
sempre que possível. Além disso, é fácil mudar de um para outro. Dada esta
função
Exercício 6.8
Definir uma lift funçãoque recebe uma função de A para B como seu argu-
mento e retorna uma função de Option<A> para Option<B> . Como de costume,
use as funções que você já definiu. A Figura 6.2 mostra como a lift função
funciona.
Figura 6.2 Levantando uma função
Dica
Solução
fun <A, B> lift(f: (A) -> B): (Opção<A>) -> Opção<B> = { it.map(f) }
A maioria de suas bibliotecas existentes não conterá funções de valor; eles conte-
rão apenas fun funções. Converter uma fun função que recebe um A como argu-
mento e retorna a B em uma função de Option<A> para Option<B> é fácil. Por
exemplo, o levantamento da função String.toUpperCase pode ser feito desta
forma:
Exercício 6.9
A lift função anterioré inútil para funções que lançam exceções. Escreva uma
versão lift que funcione com funções que lançam exceções.
Solução
fun <A, B> lift(f: (A) -> B): (Opção<A>) -> Opção<B> = {
tentar {
it.map(f)
} catch (e: Exceção) {
Opção()
}
}
fun <A, B> hLift(f: (A) -> B): (A) -> Opção<B> = {
tentar {
Opção(it).map(f)
} catch (e: Exceção) {
Opção()
}
}
Observe que essa abordagem não é útil porque a exceção é perdida. No capítulo 7,
você aprenderá como resolver esse problema.
E se você quiser usar uma função herdada com dois argumentos? Digamos que
você queira usar o Integer.parseInt(String s, int radix) método Ja-
vacom um Option<String> e um Option<Integer> . Como você pode fazer
isso? A primeira etapa é criar uma função de valor a partir desse método Java.
Isso é simples:
Eu inverti os argumentos aqui e criei uma função curry. Isso faz sentido porque
inverter os argumentos produz (aplicando apenas o primeiro argumento) uma
função que permite analisar todas as strings com um determinado radix . Isso
pode ser útil, embora não inverter os argumentos produza uma função que per-
mite analisar uma determinada string com qualquer radix . Isso geralmente é
muito menos útil, embora dependa do seu caso de uso específico:
Exercício 6.10
Dica
Solução
Aqui está a solução, usando flatMap e map . É importante entender esse padrão
e você o encontrará com frequência. Voltaremos a isso no capítulo 8:
Você vê o padrão? (O fato de a última função ser map e não flatMap se deve ape-
nas ao fato de f retornar um valor bruto. Se ela retornou um Option , você
ainda pode usar esse padrão substituindo map por flatMap .)
Composição Option instâncias não é tudo que você precisa. Em algum momento,
cada novo tipo definido deve ser composto por qualquer outro. No capítulo ante-
rior, você definiu o List tipo. Para escrever programas úteis, você precisa ser ca-
paz de compor List e Option .
Exercício 6.11
Observe que agora você deve usar o List definido no capítulo 5 e não o Kotlin
List .
Dica
Para encontrar o caminho, você pode testar a lista para ver se está vazia e, se não
estiver, fazer uma chamada recursiva para sequence . Então, lembrando dessa
foldRight and foldLeft recursão abstrata, você poderia usar uma dessas fun-
ções para implementar sequence .
Solução
Aqui está uma versão explicitamente recursiva que poderia ser usada se você ti-
vesse definido list.head() e list.tail() públicofunções em List (mas não
vai compilar porque você não tem):
Isso produz o resultado pretendido, mas é um tanto ineficiente porque a map fun-
çãoe a sequence funçãoambos invocam foldRight .
Exercício 6.12
Definir uma traverse funçãoque produz o mesmo resultado, mas invoca fol-
dRight apenas uma vez. Aqui está sua assinatura:
Não use recursão explicitamente. Prefira a foldRight função que abstrai a re-
cursão para você.
Solução
ComoEu disse no capítulo 2 e na introdução deste capítulo que Kotlin tem uma
maneira diferente de lidar com dados opcionais. Você pode então se perguntar
qual técnica deve usar - tipos anuláveis ou Option :
Os tipos anuláveis são úteis para uso interno em algumas funções, como gera-
dores, quando o retorno null indica a condição do terminal. Mas tais nulos
nunca devem vazar para fora de uma função; esses devem ser apenas para uso
local e nunca retornar, null exceto em funções locais.
Option é bom para dados verdadeiramente opcionais. Mas, geralmente, a au-
sência de dados é resultado de erros que os programadores tradicionais trata-
riam lançando e capturando exceções. Retornar None em vez de lançar uma
exceção é como capturar uma exceção e engoli-la silenciosamente. Pode não
ser um bilhão de dólareserro, mas ainda é um grande problema. Você apren-
derá no capítulo 7 como lidar com essa situação. Depois disso, dificilmente
você precisará do Option tipo de dados novamente. Mas não se preocupe;
tudo o que você aprendeu neste capítulo ainda será extremamente útil.
O Option tipo é a forma mais simples de um tipo de dado que você usará repeti-
damente. É um tipo parametrizado e vem com uma função para fazer um
Option<A> de um arquivo A . O Option tipo também tem uma flatMap fun-
çãoque podem ser usados para compor Option instâncias. Embora não seja útil
por si só, ele apresentou a você um conceito fundamental chamadomônada . Mas
List também tem as mesmas características. O importante é o que essas duas
classes têm em comum. Não se preocupe com o termo mônada ; não há nada a te-
mer. Veja-o como um padrão de design (embora seja muito maisdo queisto).
Resumo
2 Consulte
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/Socket.html .
3
A resposta de Brian Goetz à pergunta “Os getters do Java 8 devem retornar um
tipo opcional?” no Stackoverflow (12 de outubro de 2014),
https://stackoverflow.com/questions/26328555 .