Gestion des erreurs fonctionnelles dans Kotlin avec Arrow

image

Bonjour, Habr!

Tout le monde aime les exceptions d'exécution. Il n'y a pas de meilleur moyen de savoir que quelque chose n'a pas été pris en compte lors de l'écriture du code. Surtout - si des exceptions suppriment l'application parmi des millions d'utilisateurs, et cette nouvelle arrive dans un e-mail de panique du portail d'analyse. Samedi matin. Lorsque vous êtes en voyage à la campagne.

Après cela, vous pensez sérieusement à la gestion des erreurs - et quelles sont les possibilités que Kotlin nous offre?

Le premier à venir à l'esprit est le try-catch. Pour moi - une excellente option, mais elle a deux problèmes:

  1. Il s'agit après tout d'un code supplémentaire (un wrapper forcé autour du code n'affecte pas la lisibilité de la meilleure façon).
  2. Il n'est pas toujours possible (en particulier lors de l'utilisation de bibliothèques tierces) du bloc catch d'obtenir un message informatif sur la cause exacte de l'erreur.

Voyons ce en quoi try-catch transforme le code en essayant de résoudre les problèmes ci-dessus.

Par exemple, la fonction d'exécution de requête réseau la plus simple

fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } 

devient comme

 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } } 

"Ce n'est pas si mal", peut-on dire, "vous et votre kotlin voulez tout le sucre codé", ajoute-t-il (c'est une citation) - et il aura ... deux fois raison. Non, il n'y aura pas d'holivars aujourd'hui - chacun décide pour lui-même. J'ai personnellement gouverné le code de l'analyseur json auto-écrit, où l'analyse de chaque champ était enveloppée dans try-catch, tandis que chacun des blocs catch était vide. Si quelqu'un est satisfait de cet état de fait - un drapeau dans ses mains. Je veux offrir une meilleure façon.

La plupart des langages de programmation fonctionnels typés offrent deux classes pour gérer les erreurs et les exceptions: Try et Either . Essayez de gérer les exceptions, et Soit pour gérer les erreurs de logique métier.

La bibliothèque Arrow vous permet d'utiliser ces abstractions avec Kotlin. Ainsi, vous pouvez réécrire la demande ci-dessus comme suit:

 fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } } 

En quoi cette approche est-elle différente de l'utilisation de try-catch?

Tout d'abord, toute personne qui lit ce code après vous (et ils le sera probablement) pourra déjà comprendre par signature que l'exécution du code peut entraîner une erreur - et écrire un code pour le traiter. De plus, le compilateur jurera si cela n'est pas fait.

Deuxièmement, la manière dont l'erreur peut être traitée est flexible.

Dans Try, l'erreur ou le succès de l'exécution est représenté respectivement par les classes Failure et Success. Si nous voulons que la fonction retourne toujours quelque chose en cas d'erreur, nous pouvons définir la valeur par défaut:

 makeRequest(request).getOrElse { emptyList() } 

Si une gestion des erreurs plus complexe est requise, le pli vient à la rescousse:

 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } ) 

Vous pouvez utiliser la fonction de récupération - son contenu sera complètement ignoré si Try retourne le succès.

 makeRequest(request).recover { emptyList() } 

Vous pouvez utiliser pour les compréhensions (emprunté par les créateurs Arrow de Scala) si vous avez besoin de traiter le résultat Success en utilisant une séquence de commandes en appelant la fabrique .monad () sur Try:

 Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) } 

L'option ci-dessus peut être écrite sans utiliser de liaison, mais elle sera lue différemment:

 httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) } 

Au final, le résultat de la fonction peut être traité en utilisant:

 when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } 

Ainsi, en utilisant Arrow, vous pouvez remplacer la construction try-catch loin d'être idéale par quelque chose de flexible et très pratique. Un avantage supplémentaire de l'utilisation de Arrow est que, malgré le fait que la bibliothèque se positionne comme fonctionnelle, vous pouvez utiliser des abstractions individuelles à partir de là (par exemple, le même Try) tout en continuant à écrire du bon vieux code OOP. Mais je vous préviens - vous pourriez l'aimer et vous impliquer, dans quelques semaines, vous commencerez à étudier Haskell, et vos collègues cesseront bientôt de comprendre votre raisonnement sur la structure du code.

PS: ça vaut le coup :)

Source: https://habr.com/ru/post/fr435254/


All Articles