Um dos pesadelos de todo desenvolvedor, é uma NullPointerException. Tratar objetos que podem conter um valor, ou não, é chato, e muito propenso à erros. Quem nunca esqueceu de fazer um check contra valores nulos?
1
| |
ou ainda
1 2 | |
Seria bom se houvesse alguma maneira de evitar esse tipo de problema de Exceptions serem lançadas por que o desenvolvedor esqueceu de tratar um valor que possivelmente veio nulo.
Desde que comecei a me aventurar pelo mundo de linguagens funcionais, percebi que lá, eu não me preocupava com isso. E isso se devia ao fato de que eu usava um tipo de dados bem interessante: o tipo Option.
1 2 3 4 | |
Como voce pode ver, o tipo Option simplesmente encapsula um valor possivelmente nulo. E existem os tipos concretos, chamados Some e None. Abaixo a declaração de ambos.
1 2 3 4 5 6 7 8 | |
E também temos um objeto Option com o método apply que cria um valor do tipo Option com base no valor passado como parâmetro:
1 2 3 | |
Assim, podemos criar valores do tipo Option da seguinte maneira:
1 2 | |
1 2 | |
Como voces podem ver, o encapsulamento do valor é feito, mas ainda sim, podemos cair em uma Exception no caso de tentarmos acessar o value do Option. Quer dizer então que o Option não serve para nada? Não é bem assim.
O poder verdadeiro do Option está em suas higher-order functions map e flatMap. Na definição da trait Option, temos a declaração dos métodos:
1 2 3 4 5 | |
Qual a útilidade desses métodos? Simples:
- Ter acesso ao valor armazenado no
Option, se houver um; - Fazer combinações de valores do tipo
Option
Como assim?
1 2 3 4 | |
Como vimos, com os métodos flatMap e map podemos acessar o valor contido dentro do Option, também é possível transformar o valor em um outro valor do tipo Option. No exemplo acima, criamos um Some(10) e depois o transformamos, multiplicando o valor por 2. Assim, obtemos um Some(20). Logo em seguida, usamos o método map para chamarmos o método println que imprime o valor na tela. E o legal é que, caso em alguma chamada a map ou flatMap, apareça um valor null, o resultado vai ser um None. E qualquer chamada a uma dessas funções sobre um valor None, nada irá acontecer, e assim, nenhuma NullReferenceException será lançada. Legal não é?
Também é possível obter o valor contido dentro do Option através de Pattern Matching:
1 2 3 4 5 6 | |
1 2 3 4 5 6 | |
Nos exemplos acima, em um caso, o valor é um Some(10), então ele cai no primeiro caso, e imprime o valor 10. No segundo caso, como é um None, ele cai no segundo caso, e imprime “Vazio”.
Um exemplo mais “mundo real” seria:
1 2 3 | |
No exemplo acima, temos uma lista de pessoas pessoas, e chamamos o método find para tentarmos achar dentro dessa lista, uma pessoa chamada “João”. Como o João pode não existir dentro da lista, ele retorna um Option[Pessoa]. Daí, pegamos o salário do possível valor do joao com a funcão map. No final, imprimimos o valor do salário. Mas, como o resultado final pode ser um None, chamamos mais uma função interessante chamada getOrElse. O que essa função faz é simples.
1 2 3 4 5 | |
Como podemos ver, ela faz um Pattern Matching sobre o próprio valor, e caso ele seja um Some[T], retornamos o próprio valor, caso seja um None, retornamos o valor default passado como parâmetro.
Então, no exemplo do João acima, caso o João exista na lista, o seu salário será impresso na tela, caso não, será impresso o valor 0.0.
O legal do Option é que através da definição do seu tipo, e de suas higher-order functions, torna-se difícil uma NullReferenceException ser lançada. Toda vez que alguma função possa retornar um valor nulo, basta retornar um valor do tipo Option[T], ao invés de simplesmente um valor do tipo T. A partir daí, é só usar as funções map e flatMap, que elas se encarregam de tratar os casos onde o valor for nulo, propagando o None por toda a cadeia de chamadas.