Dans le post précédent, nous avons abordé les concepts de la gestion d’erreur en programmation fonctionnelle à l’aide du langage Scala.
Il est possible d’appliquer ces recettes à d’autres langages moins orientés « programmation fonctionnelle », à l’aide de librairies spécialisées émulant certaines caractéristiques propres à ce type de langages.
Kotlin et la librairie Arrow
Du côté du langage Kotlin, il existe une notion de « nullable types » qui peut s’apparenter au type Option de Scala. Le caractère ? à la fin d’un type signifie que la valeur peut être nulle. Si ce symbole n’est pas présent dans la définition d’un type, il est impossible d’affecter une valeur nulle.
Il est possible de chainer plusieurs valeurs nullables avec la méthode let :
fun giveNullableInt(x: Int): Int? {
return x + 1
}
fun giveNullableInt2(x: Int): Int? {
return x + 2
}
giveNullableInt(1)?.let { giveNullableInt2(it) } // 4 (de type Int?)
La librairie
Arrow s’inspire fortement de Scala et introduit notamment les types Either et Option (en alternative aux nullable types). Voici comment chainer deux valeurs de types Either en Kotlin :
fun giveEither(x: Int): Either<String, Int> {
return right(x + 1)
}
fun giveEither2(x: Int): Either<String, Int> {
return right(x + 2)
}
giveEither(1).flatMap { giveEither2(it) }
Il est également possible de recourir à une syntaxe proche du « for comprehension » de Scala avec Arrow, via la construction « binding » :
val combined: Either<String, Int> = binding {
val r1 = giveEither(1).bind()
val r2 = giveEither2(r1).bind()
r2
} // Right(4)
Composition asynchrone en Kotlin
Kotlin dispose d’une fonctionnalité appelée « coroutines » qui permet de créer des fonctions asynchrones sans les wrapper dans un type comme Future. Les coroutines ont également l’avantage de reposer sur des threads légers. Voici un exemple de fonctions asynchrones en Kotlin et d’un résultat composé de l’appel à ces fonctions :
suspend fun giveCoroutine(x: Int): Int {
delay(1000)
return x + 1
}
suspend fun giveCoroutine2(x: Int): Int {
delay(1000)
return x + 2
}
val asyncResult1 = async { giveCoroutine(1) }
giveCoroutine2(asyncResult1.await()) //4
On peut voir que la composition de fonctions asynchrones en Kotlin se fait comme une composition de fonctions ordinaires.
Il est donc possible de gérer l’absence de valeur et l’asynchronicité grâce aux nullables types aux coroutines, sans utiliser de wrappers autour de nos types.
Note importante : la gestion d’erreur des coroutines fonctionne avec une utilisation classique de blocs try/catch. Arrow propose une approche plus fonctionnelle, détaillée ici. Arrow dispose également d’une monade IO permettant de gérer des opérations asynchrones d’une manière plus fonctionnelle.
Même si on a pu s’en passer dans les exemples précédents, les types wrappers (et notamment les monades) ont leur intérêt pour des opérations plus complexes. Voyons maintenant comment chainer des types wrappés dans un Either grâce à Arrow, dans un contexte asynchrone :
suspend fun giveEitherCoroutine(x: Int): Either<String, Int> {
delay(1000)
return right(x + 1)
}
suspend fun giveEitherCoroutine2(x: Int): Either<String, Int> {
delay(1000)
return right(x + 2)
}
val asyncResult1 = async { giveEitherCoroutine(1) }
asyncResult1.await().flatMap { r1 ->
giveEitherCoroutine2(r1)
} //Right(4)
Enfin, Arrow fournit aussi un type Validated permettant d’accumuler les erreurs.
TypeScript et la librairie fp-ts
Dans le même esprit, la librairie fp-ts tente d’apporter la programmation fonctionnelle au langage TypeScript.
On peut par exemple utiliser les types Option et Either, et les combiner avec la fonction chain :
function giveOption(x: number ): Option<number> {
return some(x + 1)
}
function giveOption2(x: number ): Option<number> {
return some(x + 2)
}
giveOption(1).chain(giveOption2) //some(4)
function giveEither(x: number ): Either<string, number> {
return right(x + 1)
}
function giveEither2(x: number ): Either<string, number> {
return right(x + 2)
}
giveEither(1).chain(giveEither2) //right(4)
Composition asynchrone en TypeScript
TypeScript dispose d’un type Promise permettant de gérer des résultats asynchrones :
async function givePromise(x: number ): Promise<number> {
const result = await x +1
return result
}
async function givePromise2(x: number ): Promise<number> {
const result = await x +2
return result
}
const combined: Promise<number> = givePromise(1).then(givePromise2)
Il est possible de combiner des promises avec le type Either en utilisant TaskEither, à partir d’un traitement asynchrone dont on encapsule les erreurs potentielles ou à partir d’une valeur de type Either :
//création d'un TaskEither à partir de Promise
function giveTaskEither(x: number): TaskEither<string, number> {
return tryCatch(() => giveIntAsync(x), reason => "oops " + reason)
}
//création d'un TaskEither à partir d'Either
function giveTaskEither2(x: number ): TaskEither<string, number> {
return fromEither(giveEither2(x))
}
const combined: Promise<Either<string, number>> = giveTaskEither(1).chain(giveTaskEither2).run()
Et pour accumuler les erreurs, fp-ts fournit également un type nommé Validation.
Conclusion
En conclusion, ces 2 librairies nous offrent la possibilité de gérer les erreurs dans notre code en adoptant une approche fonctionnelle et en facilitant la composabilité. Cela nous permet d’obtenir un code plus lisible, plus modulaire et reposant sur des types plus fiables.
Retour aux articles