Introduction à la flèche à Kotlin
1. Vue d'ensemble
Arrow est une bibliothèque fusionnée deKΛTEGORY etfunKTionale.
Dans ce didacticiel, nous allons examiner les bases d'Arrow et comment il peut nous aider à exploiter la puissance de la programmation fonctionnelle dans Kotlin.
Nous discuterons des types de données dans le package principal et étudierons un cas d'utilisation de la gestion des erreurs.
2. Dépendance Maven
Pour inclure Arrow dans notre projet, nous devons ajouterthe arrow-core dependency:
io.arrow-kt
arrow-core
0.7.3
3. Types de données fonctionnelles
Commençons par examiner les types de données dans le module principal.
3.1. Introduction aux monades
Certains des types de données discutés ici sont des monades. Très fondamentalement, les monades ont les propriétés suivantes:
-
Il s’agit d’un type de données spécial recouvrant essentiellement une ou plusieurs valeurs brutes.
-
Ils ont trois méthodes publiques:
-
une méthode d'usine pour envelopper des valeurs
-
map
-
flatMap
-
-
Ces méthodes agissentnicely, c'est-à-dire qu'elles n'ont pas d'effets secondaires.
Dans le monde Java, les tableaux et les flux sont des Monades mais desOptional isn’t. Pour en savoir plus sur les Monades, peut-être unbag of peanuts can help.
Voyons maintenant le premier type de données du modulearrow-core.
3.2. Id
Id est le wrapper le plus simple d'Arrow.
Nous pouvons le créer avec un constructeur ou avec une méthode de fabrication:
val id = Id("foo")
val justId = Id.just("foo");
Et, il a une méthodeextract pour récupérer la valeur encapsulée:
Assert.assertEquals("foo", id.extract())
Assert.assertEquals(justId, id)
La classeId satisfait aux exigences du modèle Monad.
3.3. Option
Option est un type de données pour modéliser une valeur qui peut ne pas être présente, similaire à facultatif de Java.
Et même s'il ne s'agit pas techniquement d'une Monade, c'est toujours très utile.
Il peut contenir deux types: le swrapperSome autour de la valeur ouNone lorsqu'il n'a pas de valeur.
Nous avons plusieurs façons de créer unOption:
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,, c'est-à-dire que la méthode d'usine et le constructeur se comportent différemment pournull:
val constructor : Option = Option(null)
val fromNullable : Option = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)
Nous préférons le second car il n’a pas de risqueKotlinNullPointerException :
try {
constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
fromNullable.map { s -> s!!.length }
}
3.3. Either
Comme nous l'avons vu précédemment,Option peut avoir aucune valeur (None) ou une valeur (Some).
Either va plus loin sur ce chemin et peut avoir l'une des deux valeurs. Either a deux paramètres génériques pour le type des deux valeurs qui sont notéesright etleft:
val rightOnly : Either = Either.right(42)
val leftOnly : Either = Either.left("foo")
Cette classe est conçue pour êtreright-biased. La branche de droite doit donc contenir la valeur commerciale, par exemple le résultat d’un calcul. La branche de gauche peut contenir un message d'erreur ou même une exception.
Par conséquent, la méthode d'extraction de valeur (getOrElse) est conçue vers la droite:
Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })
Même les méthodesmap etflatMap sont conçues pour fonctionner avec le côté droit et ignorer le côté gauche:
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())
Nous allons étudier comment utiliserEither pour la gestion des erreurs dans la section 4.
3.4. Eval
Eval est une Monade conçue pour contrôler l'évaluation des opérations. It has a built-in support for memoization and eager and lazy evaluation.
Avec la méthode d'usinenow, nous pouvons créer une instanceEval à partir de valeurs déjà calculées:
val now = Eval.now(1)
Les opérationsmap etflatMap seront exécutées paresseusement:
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)
Comme nous pouvons le voir, lescounter ne changent qu'après l'appel de la méthodevalue .
La méthodelater factory créera une instanceEval à partir d'une fonction. L'évaluation sera différée jusqu'à l'appel devalue et le résultat sera mémorisé:
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)
La troisième usine estalways. Il crée une instance deEval qui recalculera la fonction donnée à chaque fois que levalue est appelé:
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. Modèles de gestion des erreurs avec des types de données fonctionnels
La gestion des erreurs en lançant des exceptions présente plusieurs inconvénients.
Pour les méthodes qui échouent souvent et de manière prévisible, comme l'analyse des entrées utilisateur sous forme de nombre, il est coûteux et inutile de lever des exceptions. La plus grande partie du coût provient de la méthodefillInStackTrace. En effet, dans les frameworks modernes, la trace de pile peut devenir ridiculement longue avec étonnamment peu d'informations sur la logique métier.
De plus, la gestion des exceptions vérifiées peut facilement compliquer inutilement le code du client. D'autre part, avec les exceptions d'exécution, l'appelant n'a aucune information sur la possibilité d'une exception.
Ensuite, nous allons mettre en œuvre une solution pour savoir si le plus grand diviseur du nombre d'entrée pair est un nombre carré. L'entrée de l'utilisateur arrivera sous forme de chaîne. Parallèlement à cet exemple, nous examinerons comment les types de données d'Arrow peuvent contribuer à la gestion des erreurs.
4.1. Gestion des erreurs avecOption
Tout d'abord, nous analysons la chaîne d'entrée comme un entier.
Heureusement, Kotlin a une méthode pratique et sans danger pour les exceptions:
fun parseInput(s : String) : Option = Option.fromNullable(s.toIntOrNull())
Nous enveloppons le résultat de l'analyse dans unOption. Ensuite, nous transformerons cette valeur initiale avec une logique personnalisée:
fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...
Grâce à la conception deOption, notre logique métier ne sera pas encombrée par la gestion des exceptions et les branches if-else:
fun computeWithOption(input : String) : Option {
return parseInput(input)
.filter(::isEven)
.map(::biggestDivisor)
.map(::isSquareNumber)
}
Comme nous pouvons le voir, il s’agit d’un code métier pur sans le fardeau des détails techniques.
Voyons comment un client peut travailler avec le résultat:
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}"
}
}
C'est génial, mais le client n'a pas d'informations détaillées sur ce qui n'allait pas avec l'entrée.
Voyons maintenant comment nous pouvons fournir une description plus détaillée d'un cas d'erreur avecEither.
4.2 Error Handling with Either
Nous avons plusieurs options pour renvoyer des informations sur le cas d'erreur avecEither. Sur le côté gauche, nous pourrions inclure un message String, un code d'erreur ou même une exception.
Pour l'instant, nous créons une classe scellée à cet effet:
sealed class ComputeProblem {
object OddNumber : ComputeProblem()
object NotANumber : ComputeProblem()
}
Nous incluons cette classe dans lesEither renvoyés. Dans la méthode d'analyse, nous utiliserons la fonction d'usine decond:
Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)
Donc, au lieu deOption, we’utiliseraEither in notre méthodeparseInput:
fun parseInput(s : String) : Either =
Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )
Cela signifie que lesEither seront renseignés aveceither le numéro ou l'objet d'erreur.
Toutes les autres fonctions seront les mêmes que précédemment. Cependant, la méthodefilter est différente pourEither. Il nécessite non seulement un prédicat mais un fournisseur du côté gauche pour la fausse branche du prédicat:
fun computeWithEither(input : String) : Either {
return parseInput(input)
.filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
.map (::biggestDivisor)
.map (::isSquareNumber)
}
En effet, nous devons fournir l'autre côté desEither, dans le cas où notre filtre renvoiefalse.
Maintenant, le client saura exactement ce qui n'allait pas avec ses commentaires:
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. Conclusion
La bibliothèque Arrow a été créée pour prendre en charge les fonctionnalités fonctionnelles de Kotlin. Nous avons examiné les types de données fournis dans le package arrow-core. Ensuite, nous avons utiliséOptional etEither pour la gestion des erreurs de style fonctionnel.
Comme toujours, le code est disponibleover on GitHub.