Introdução ao Arrow no Kotlin
1. Visão geral
A seta é uma biblioteca mesclada deKΛTEGORYefunKTionale.
Neste tutorial, veremos os fundamentos do Arrow e como ele pode nos ajudar a aproveitar o poder da programação funcional em Kotlin.
Discutiremos os tipos de dados no pacote principal e investigaremos um caso de uso sobre tratamento de erros.
2. Dependência do Maven
Para incluir Arrow em nosso projeto, temos que adicionarthe arrow-core dependency:
io.arrow-kt
arrow-core
0.7.3
3. Tipos de dados funcionais
Vamos começar investigando os tipos de dados no módulo principal.
3.1. Introdução às Mônadas
Alguns dos tipos de dados discutidos aqui são Mônadas. Basicamente, as mônadas têm as seguintes propriedades:
-
Eles são um tipo de dados especial que é basicamente um invólucro em torno de um ou mais valores brutos
-
Eles têm três métodos públicos:
-
um método de fábrica para agrupar valores
-
map
-
flatMap
-
-
Esses métodos atuamnicely, ou seja, não apresentam efeitos colaterais.
No mundo Java, matrizes e fluxos são Mônadas, masOptional isn’t. Para mais informações sobre Mônadas, talvez abag of peanuts can help.
Agora vamos ver o primeiro tipo de dados do móduloarrow-core.
3.2. Id
Id é o wrapper mais simples do Arrow.
Podemos criá-lo com um construtor ou com um método de fábrica:
val id = Id("foo")
val justId = Id.just("foo");
E tem um métodoextract para recuperar o valor empacotado:
Assert.assertEquals("foo", id.extract())
Assert.assertEquals(justId, id)
A classeId atende aos requisitos do padrão Monad.
3.3. Option
Option é um tipo de dados para modelar um valor que pode não estar presente, semelhante ao opcional de Java.
E embora não seja tecnicamente uma Mônada, ainda é muito útil.
Ele pode conter dois tipos: O swrapperSome em torno do valor ouNone quando ele não tem valor.
Temos algumas maneiras diferentes de criar umOption:
val factory = Option.just(42)
val constructor = Option(42)
val emptyOptional = Option.empty()
val fromNullable = Option.fromNullable(null)
Assert.assertEquals(42, factory.getOrElse { -1 })
Assert.assertEquals(factory, constructor)
Assert.assertEquals(emptyOptional, fromNullable)
Now, there is a tricky bit here,, que é que o método de fábrica e o construtor se comportam de maneira diferente paranull:
val constructor : Option = Option(null)
val fromNullable : Option = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)
Preferimos o segundo porque não temKotlinNullPointerException risk:
try {
constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
fromNullable.map { s -> s!!.length }
}
3.3. Either
Como vimos anteriormente,Option pode não ter valor (None) ou algum valor (Some).
Either avança neste caminho e pode ter um de dois valores. Either tem dois parâmetros genéricos para o tipo dos dois valores que são denotados comorighteleft:
val rightOnly : Either = Either.right(42)
val leftOnly : Either = Either.left("foo")
Esta classe foi projetada para serright-biased. Portanto, o ramo certo deve conter o valor comercial, por exemplo, o resultado de alguns cálculos. O ramo esquerdo pode conter uma mensagem de erro ou até uma exceção.
Portanto, o método de extração de valor (getOrElse) é projetado para o lado direito:
Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })
Mesmo os métodosmapeflatMap são projetados para funcionar com o lado direito e pular o lado esquerdo:
Assert.assertEquals(0, rightOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertTrue(rightOnly.flatMap { Either.Right(it % 2) }.isRight())
Assert.assertTrue(leftOnly.flatMap { Either.Right(it % 2) }.isLeft())
Investigaremos como usarEither para tratamento de erros na seção 4.
3.4. Eval
Eval é uma Mônada projetada para controlar a avaliação das operações. It has a built-in support for memoization and eager and lazy evaluation.
Com o método de fábricanow, podemos criar uma instânciaEval a partir de valores já calculados:
val now = Eval.now(1)
As operaçõesmap eflatMap serão executadas lentamente:
var counter : Int = 0
val map = now.map { x -> counter++; x+1 }
Assert.assertEquals(0, counter)
val extract = map.value()
Assert.assertEquals(2, extract)
Assert.assertEquals(1, counter)
Como podemos ver, ocounter só muda depois que o métodovalue é invocado.
O métodolater factory criará uma instânciaEval de uma função. A avaliação será adiada até a invocação devaluee o resultado será memorizado:
var counter : Int = 0
val later = Eval.later { counter++; counter }
Assert.assertEquals(0, counter)
val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)
val secondValue = later.value()
Assert.assertEquals(1, secondValue)
Assert.assertEquals(1, counter)
A terceira fábrica éalways. Ele cria uma instânciaEval que irá recomputar a função dada cada vez quevalue é invocado:
var counter : Int = 0
val later = Eval.always { counter++; counter }
Assert.assertEquals(0, counter)
val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)
val secondValue = later.value()
Assert.assertEquals(2, secondValue)
Assert.assertEquals(2, counter)
4. Padrões de tratamento de erros com tipos de dados funcionais
O tratamento de erros por meio do lançamento de exceções tem várias desvantagens.
Para métodos que falham com frequência e de forma previsível, como analisar a entrada do usuário como um número, é caro e desnecessário lançar exceções. A maior parte do custo vem do métodofillInStackTrace. De fato, nas estruturas modernas, o rastreamento da pilha pode crescer ridiculamente longo com surpreendentemente pouca informação sobre a lógica de negócios.
Além disso, o tratamento de exceções verificadas pode facilmente tornar o código do cliente desnecessariamente complicado. Por outro lado, com exceções de tempo de execução, o chamador não tem informações sobre a possibilidade de uma exceção.
A seguir, implementaremos uma solução para descobrir se o maior divisor do número par de entrada é um número quadrado. A entrada do usuário chegará como uma String. Junto com este exemplo, vamos investigar como os tipos de dados de Arrow podem ajudar no tratamento de erros
4.1. Tratamento de erros comOption
Primeiro, analisamos a String de entrada como um número inteiro.
Felizmente, o Kotlin tem um método prático e seguro contra exceções:
fun parseInput(s : String) : Option = Option.fromNullable(s.toIntOrNull())
Envolvemos o resultado da análise em umOption. Então, vamos transformar esse valor inicial com alguma lógica personalizada:
fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...
Graças ao design deOption, nossa lógica de negócios não ficará confusa com tratamento de exceções e ramificações if-else:
fun computeWithOption(input : String) : Option {
return parseInput(input)
.filter(::isEven)
.map(::biggestDivisor)
.map(::isSquareNumber)
}
Como podemos ver, é puro código de negócios, sem a carga de detalhes técnicos.
Vamos ver como um cliente pode trabalhar com o resultado:
fun computeWithOptionClient(input : String) : String {
val computeOption = computeWithOption(input)
return when(computeOption) {
is None -> "Not an even number!"
is Some -> "The greatest divisor is square number: ${computeOption.t}"
}
}
Isso é ótimo, mas o cliente não tem informações detalhadas sobre o que havia de errado com a entrada.
Agora, vamos ver como podemos fornecer uma descrição mais detalhada de um caso de erro comEither.
4.2 Error Handling with Either
Temos várias opções para retornar informações sobre o caso de erro comEither. No lado esquerdo, podemos incluir uma mensagem String, código de erro ou até uma exceção.
Por enquanto, criamos uma classe selada para este fim:
sealed class ComputeProblem {
object OddNumber : ComputeProblem()
object NotANumber : ComputeProblem()
}
Incluímos essa classe noEither retornado. No método de análise, usaremos a função de fábricacond:
Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)
Então, em vez deOption, we’ll usaráEither in nosso métodoparseInput:
fun parseInput(s : String) : Either =
Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )
Isso significa queEither será preenchido comeither o número ou o objeto de erro.
Todas as outras funções serão as mesmas de antes. No entanto, o métodofilter é diferente paraEither. Requer não apenas um predicado, mas um provedor do lado esquerdo para o falso branch do predicado:
fun computeWithEither(input : String) : Either {
return parseInput(input)
.filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
.map (::biggestDivisor)
.map (::isSquareNumber)
}
Isso porque, precisamos fornecer o outro lado deEither, no caso de nosso filtro retornarfalse.
Agora o cliente saberá exatamente o que havia de errado com a entrada deles:
fun computeWithEitherClient(input : String) {
val computeWithEither = computeWithEither(input)
when(computeWithEither) {
is Either.Right -> "The greatest divisor is square number: ${computeWithEither.b}"
is Either.Left -> when(computeWithEither.a) {
is ComputeProblem.NotANumber -> "Wrong input! Not a number!"
is ComputeProblem.OddNumber -> "It is an odd number!"
}
}
}
5. Conclusão
A biblioteca Arrow foi criada para oferecer suporte a recursos funcionais no Kotlin. Investigamos os tipos de dados fornecidos no pacote arrow-core. Em seguida, usamosOptionaleEither para tratamento de erros de estilo funcional.
Como sempre, o código está disponívelover on GitHub.