C'est donc aujourd'hui que sort officiellement Go 1.18, avec le support tant attendu des Generics.
Naïf que je suis, je me dis :
Cool! On va enfin pouvoir implémenter des types
Option
etResult
pour avoir enfin une gestion d'erreur potable.
Allez, je me lance. On commence par le type Option
(ou Maybe Monad pour les intimes).
Tout d'abord, on se créé 2 structures, None
et Some[T]
:
type None struct {}
type Some[T any] {
value T
}
On crée ensuite l'interface qui encapsule ces 2 types :
type Option[T any] interface {
None | Some[T]
}
Et on crée les constructeurs :
func Nothing() None {
return None {}
}
func Just[T any](val T) Some[T] {
return Some[T] { value: val }
}
Puis, on créé une petite fonction HasValue
qui retourne vrai ou faux selon le type de la monade :
func (opt None) HasValue() bool {
return false
}
func (opt Some[T]) HasValue() bool {
return true
}
Jusque là, tout va bien. Maintenant, on va créer la fonction Map
qui transforme un Option[T]
en Option[U]
grâce à une fonction qui transforme T
en U
.
Le principe est ultra simple :
- Si la monade est de type
None
on retourneNone
- Si la monade est de type
Some[T]
on applique la fonctionf : T -> U
sur la valeur contenue, et on retourneSome[U]
Voici l'implémentation :
func (opt None) Map[U any](f func(T) U) Option[U] {
return Nothing()
}
func (opt Some[T]) Map[U any](f func(T) U) Option[U] {
return Just(f(opt.value))
}
Plutôt simple non ?
C'est dommage, ça marchera pas :
method must have no type parameter
Et c'est un choix volontaire --> https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods
Alors, oui, je pourrais faire :
func MapOption[T any, U any](opt Option[T], f func(T) U) Option[U] {
// ...
}
Mais non, Option[T]
ne peut être utilisé que comme type constraint.
Ok, alors :
func MapOption[T any, U any, OT Option[T], OU Option[U])(opt OT, f func(T) U) OU {
switch opt.(type) {
// ...
}
}
Toujours pas, on ne peut pas faire de switch sur le type sous-jacent d'un générique.
Ok, donc :
func MapOption[T any, U any](opt interface{}, f func(T) U) interface{} {
switch opt.(type) {
case Some[T]:
val := opt.(Some[T]).value
return Just[U](f(val))
default:
return Nothing()
}
}
Oui, ça marche. Mais on perd tout l'intérêt des generics. Et il va falloir s'amuser à cast les interface{}
dans le bon type à chaque appel.
Donc pour l'instant, AMHA, les generics ne servent pas à grand chose si ce n'est composer des interfaces.
# Kamoulox !
Posté par devnewton 🍺 (site web personnel) . Évalué à 10.
Si je lis bien la justification initiale, l'ajout de la généricité dans Go est surtout fait pour les structures de données et les algos qui les manipulent, pas pour gérer les erreurs de façon
gruikélégante.Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.
[^] # Re: Kamoulox !
Posté par YBoy360 (site web personnel) . Évalué à 3.
Je n'ai pas bien compris ce qu'était une gestion élégante d'erreur avec
un unionune interface encapsulant 2 types…Ce n'est pas plus propre de séparer le résultat et la gestion d'erreur ?
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 10.
L'avantage du type
Result<Data, Error>
c'est les opérations que tu peux chaîner.Result<T, E> -> Result<U, E> -> Result<V, E> -> ...
Tu peux donc découpler la gestion d'erreur de ton algorithme et ce sans avoir besoin d'exception et de "jump" dans le code.
Un exemple en Elixir avec ma lib rustic_result :
En gros :
map : Result<T, E> -> Result<U, E>
avecf : T -> U
map_err : Result<T, E> -> Result<T, E2>
avecf : E -> E2
and_then : Result<T, E> -> Result<U, E2>
avecf : T -> Result<U, E2>
or_else : Result<T, E> -> Result<U, E2>
avecf : E -> Result<U, E2>
Le principe est de te permettre, grâce à ce type et ses opérations, de composer des "computations" et d'en récupérer un type final cohérent que tu peux traiter correctement.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par xoddark . Évalué à 3.
Je suis en phase d'apprentissage de Rust, et ce système est nouveau pour moi.
Mais de cette expérience encore fraiche, je dirais que cette façon de rassembler les données valide et d'erreur permet de plus facilement et proprement séparer le code logique du code de gestion d'erreurs.
[^] # Re: Kamoulox !
Posté par Anthony Jaguenaud . Évalué à 6.
Le principe en F# de railway programming.
[^] # Re: Kamoulox !
Posté par Letho . Évalué à 2.
Merci pour l'article, j'utilisais ce pattern sans en connaître le nom.
De façon générale, les publications de ce site ont l'air super intéressantes ; ça me donnerait presque envie de me mettre à F# ou OCaml.
[^] # Re: Kamoulox !
Posté par YBoy360 (site web personnel) . Évalué à 5.
C'est inspirant cette façon de faire des présentations… 1h pour ne pas faire de try catch.
Ce qui est intéressant c'est la volonté de formuler tous les cas de figure pour la gestion d'erreurs et d'y associer une syntaxe.
Ce qui me gène avec cette approche :
En ce qui me concerne, nous utilisons groovy et Java, qui propose avec les enum des possibilités proches. Pour le cas présenter (validation d'un objet, sequence de traitement, gestion des erreurs associées) :
Les exceptions constituent les erreurs non prévisibles, comme un problème réseau, disque plein, et d'autres aspect, comme la sécurité. Je comprends mal l'intérêt de ne pas utiliser les exceptions..
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 5.
Quand tu lis la documentation d'une fonction, si elle retourne un Result, tu peux tout de suite savoir les cas d'échec possibles. Et le système de type du langage peut vérifier que tout les cas sont gérés.
C'est pas le cas avec les exceptions. Les throws que tu fais ne font pas partie de la signature de la fonction.
Les exceptions sont une forme de early return, souvent comparé à goto. Pour ma part je trouve les 2 concepts complémentaire, cf Elixir ou on utilise
{:ok, val}
ou{:error, reason}
pour ce qui est prévisible etraise
/rescue
pour ce qui ne l'est pas.Dans certains cas je peux considérer qu'une requête HTTP qui retourne une 404 est prévisible mais qu'un network unreachable ne l'est pas : est-ce que la ressource existe ?
Dans d'autres cas, je considère que toute erreur est imprévisible : j'ai besoin de tel document
Dans le premier cas, si je reçois un
{:error, 404}
je peux retournerfalse
et laisser l'exception network unreachable se propager.Dans le second cas je transforme le
{:error, reason}
en exception (unwrap).Les Result et les exceptions permettent tout deux de séparer le code et la gestion d'erreur, mais la sémantique, les performances, et l'intégration au système de type sont radicalement différent.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par Anthony Jaguenaud . Évalué à 4.
Je ne suis pas d’accord. Si les interfaces que tu utilises sont correctement définies, tu dois pouvoir connaître la liste des exceptions. Donc, des erreurs. Récupérer une exception
networkUnreachable
ou une erreurNetworkUnreachable
, je ne vois pas trop la différence.Le principal intérêt des exceptions, àmha, dans les langages impératifs, c’est de pouvoir balancer l’erreur et la récupérer à l’endroit le plus propice, sans faire de cascade d’erreur, de cas particulier juste dans l’optique de la rebalancer à l’appelant. Le pendant de ça, c’est que rien ne garantit que quelqu’un récupère l’erreur.
Avec les types permettant le Railway programming, dans les langages fonctionnels, si tu ajoutes un cas d’erreur, tous les
case
,match
où le type est utilisé doit être complet. Donc le compilateur te le dira si tu oublies de gérer un cas d’erreur.[^] # Re: Kamoulox !
Posté par devnewton 🍺 (site web personnel) . Évalué à 5.
En Java, tu es obligé d'attraper la plupart des exceptions.
Malheureusement il y a des exceptions "unchecked".
On retrouve le même horrible principe en Go avec error versus panic :(
Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 4. Dernière modification le 17 mars 2022 à 15:05.
Sauf que tu as
recover()
en Go pour "catch" le panic.Ce qui veut dire que panic/recover c'est le try/catch de Go, mais en plus moche.
https://go.dev/blog/defer-panic-and-recover
EDIT: A moins que c'est justement ce qui tu voulais dire, auquel cas, ignore mon message.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par woffer 🐧 . Évalué à 3.
Et oui, en beaucoup plus moche, car si rien ne récupère ton panic (via le defer recover()) et bien ton programme s’arrête immédiatement avec une belle stacktrace. Ça peut être très gênant :)
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 5.
Pas sûr de comprendre.
Si t'as rien qui catch ton exception, ça remonte aussi en haut de la stack et fini par quitter le programme anormalement.
Je disais que le panic/recovery était plus moche car au moins avec de vrai exceptions tu peux contrôler précisément ou et quand le recovery code s'exécute.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par woffer 🐧 . Évalué à 4.
On est d'accord sur la mocheté des panic/recover mais avec cette petite joyeuseté supplémentaire.
Bon, en Go, je ne panique pratiquement jamais, sauf si c'est une erreur fatale au démarrage (ex: impossible de binder un port ou de se connecter à une BDD ou autres choses). Sinon je gère le cas non passant via des erreurs (le célèbre if err != nil) avec le cas passant sur le côté gauche (https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88). Ca m'autorise en relecture de folder facilement le code pour me concentrer sur le cas passant.
[^] # Re: Kamoulox !
Posté par xcomcmdr . Évalué à 2.
C'est pas plutôt uniquement pour les checked exceptions ?
"Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 3.
C'est justement pour ça qu'il précise juste après l'existance des exceptions "unchecked".
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par xcomcmdr . Évalué à 2.
Bah oui mais c'est loin d'être la majorité des exceptions en Java.
"Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 2. Dernière modification le 17 mars 2022 à 23:32.
Premièrement, c'est pas le sujet, le concept de checked exception existe, qu'elles soient utilisées ou non en pratique est un autre débat.
Deuxièmement, c'est principalement les
RuntimeException
(et classes filles) qui sont unchecked.https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par xcomcmdr . Évalué à 1. Dernière modification le 20 mars 2022 à 11:45.
Ouais ben je suis bien content de pas avoir des checked exceptions, ça n'a aucun sens 99% du temps.
"Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)
[^] # Re: Kamoulox !
Posté par YBoy360 (site web personnel) . Évalué à 3.
Pas en Groovy. Dans le cadre d'un serveur d'application qui intègre un connecteur FreeCAD, Solr, LibreOffice, sans compter la gestion des données persistantes, la problématique de la sécurité, la validation des données… c'est peut-être un avantage. Tu n'as certainement pas envie de traiter tous les cas de figure si le formulaire contient un fichier à uploader et peu lever un nombre d'exception conséquent lors de la sauvegarde de ce formulaire.
D'ailleurs, vu par l'utilisateur, le cas ou une exception est non catchée sur un serveur d'application c'est que la requête retourne un code 501 et que les données persistantes ne sont pas modifiées. C'est donc "limité". L'application continue de tourner. C'est difficile d'anticiper un comportement correcte suite à certaines erreurs. Moi, c'est mon tél ou ma boite mail qui vont recevoir les insultes, personne ne va en mourir dans mon cas (à moins d'être susceptible ou dépressif). Mais je ne vois pas comment on peut faire mieux.
Il n'y a quasiment pas de try … catch dans notre code, excepté pour ne pas interrompre l'initialisation du serveur si l'erreur est tolérable, lors de l'initialisation des services, lorsque le service est initialisé AVANT son utilisation (pas de lazy-init). Bref, au final, je n'ai pas quasiment pas de code de gestion d'erreur, mais nombre d'erreur soit sont traitées par le framework lui-même, soit on ne peut rien en faire et seulement prévenir l'administrateur…
[^] # Re: Kamoulox !
Posté par groumly . Évalué à 4.
Si ça marche pour toi, tant mieux, mais je doute que ça soit le cas pour beaucoup d’applis.
Sur les erreurs dures, que tu ne peux pas corriger, t’as probablement envie de mapper les erreurs en fonction du contexte. Y’a une différence entre « tu m’as passer un uuid quand je m’attend à un int », auquel cas tu veux retourne un 400 bad request, et un « je me suis pété une NPE, et je veux retourner un 500 », avec tout un spectre entre les deux (404 not found, 503, 406, 429, que sais je encore), et tu peux vouloir remonter un code d’erreur applicatif a ton client aussi.
Et en fonction du besoin, certaine partie du traitement peuvent être optionnelles, et la requête peut être complétée sans forcément tout faire. Typiquement, sur un read only qui agrège des données de multiples sources, ce qui est somme toute très courant dans un monde soa. Et la croit moi, tu veux les catcher ces exceptions la.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Kamoulox !
Posté par groumly . Évalué à 5.
Je sais pas trop ce que tu veux dire par « aspect performance », mais lancer une exception c’est loin d’être cheap, le stack unwinding, capturer la stack trace etc a un coût certain. En pratique, pour beaucoup d’application, ça va pas faire une différence notable, mais c’est pas gratuit, que ce soit en c++ ou en java.
A l’inverse, retourner un struct avec erreur xor résultat ne coute presque rien, c’est traité comme un return de base, que tu doit bien faire à un moment ou un autre.
Le coût de performance se transfère surtout sur la façon d’écrire le code, et la, t’as 2 approches, grosse modo:
if (result.error)
(ce qui peut vite devenir assez moche en fonction du code que t’écrit),if (result.error) goto exceptionHandlingBlock
. C’est ce que fait Swift notamment, avec un try/catch qui n’est qu’une façon de différencier le cas d’erreur du cas normal au niveau syntaxique, mais en utilisant un Result sous le capot.Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Kamoulox !
Posté par David Delassus (site web personnel) . Évalué à 4.
Totalement d'accord. Et c'est aussi plus complexe à implémenter correctement dans un interpréteur / compilateur.
C'est la méthode Erlang/Elixir. Mais ça génère pas trop de boilerplate grâce au pattern matching.
C'est plus ou moins la méthode de Rust avec le
?
que tu mets après les appels de fonction qui retournent cette structure.https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Kamoulox !
Posté par barmic 🦦 . Évalué à 4.
Pour utiliser abondamment les autres approches (try/catch, résultat genre optional monadique sur le quel tu fais des map/filter/flatmap/etc), je trouve le pattern matching très élégant.
Il est d'une part bien plus souple, tu ne discrimine pas forcément les erreurs/résulats, mais les résultats vides par exemple.
En terme de performance ça peut devenir génial avec de l'inline de code et si le compilateur a le droit de supprimer cette structure et tu peux te retrouver avec des appels sans gestion d'erreur si le compilateur a pu déduire que l'erreur n'était pas possible (ou simplement que le cas n'est pas possible) dans cet appel là. Dans le pire cas c'est une
if
très simple.https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Kamoulox !
Posté par YBoy360 (site web personnel) . Évalué à 2.
Il y a 2 aspects liés à la gestion des exceptions :
En C++, le coût de gestion était élevé à mon époque, tout le monde n'activait pas la gestion de celles-ci (d'où des incompatibilités sur certaines plateformes). En gros, ça revient à ajouter des return spéciaux supplémentaires, et l'une des règles de codage en C++ était de minimiser le nombre de return dans le corps des méthodes. Je crois qu'en Java, le coût de gestion est plus négligeable car il peut y avoir des optimisations au runtime, et la Jvm est stack-based (un peu comme du C++ sur du x86), donc peu d'impact d'un return.
Le lancement d'une exception dans le cas normal arrive rarement. Donc même si le coût est élevé, tu perds au final peu de performances. C'est différent de systématiquement encapsuler ton résultat. Mais oui, c'est pas forcément significatif en terme d'impacte sur la plupart du code que l'on rencontre.
# Go ou golang ?
Posté par Joalland . Évalué à 6.
J'ai pas bien compris, le nom du langage c'est go ou golang ? Les gens disent comment dans la vraie vie ?
[^] # Re: Go ou golang ?
Posté par David Delassus (site web personnel) . Évalué à 8.
Le nom c'est Go.
Mais quand je fais des recherches google, les résultats sont plus pertinents en utilisant le terme "golang". Du coup je mélange un peu les deux.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Go ou golang ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 6.
Le nom du langage est bien « Go » mais le site officiel s'appelle « golang » et on fait référence au langage comme ça (en fait « go-lang » qui en devenant très commun perd son tiret de mot composé, comme « e-mail » et autres) dans beaucoup d'endroits. Ça rend la recherche plus facile dans les moteurs de recherche …parce-que s'appeler d'un mot (verbe) courant et si court, c'est le risque de noyade dans la flopée de page (dont l'emploie du mot est fort légitime d'ailleurs.)
La faute à un choix de nom peu heureux, mais c'est pas le pire (n'est-ce pas C, D, Eiffel, J, Julia, R, et j'en passe ?)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
# Approche prudente ?
Posté par woffer 🐧 . Évalué à 7.
Dans la référence que tu as donnée à savoir https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods, on peut y lire :
Donc, j'ai l'impression qu'ils se laissent le temps pour identifier les effets de bord éventuels et/ou comment l’implémenter.
# Conteneurs
Posté par n_e . Évalué à 5.
Je dirais qu'ils servent surtout à gérer les conteneurs et les fonctions qui opèrent dessus en ignorant le contenu de façon simple, type-safe et sans codegen.
Par exemple, le package
sort
permet de trier un slice en utilisant la fonction https://pkg.go.dev/sort#Slice qui n'est pas vraiment ergonomique (je dirais même pas sûre).Avec les génériques on pourrait avoir
func Slice[T any](x []T, less func(xi, xj *T) bool)
à la place.Pour prendre un exemple plus compliqué, l'outil dataloaden permet de générer du code pour un pattern "dataloader", ie. permettre d'avoir une fonction
fn(T): U
dans l'interface publique, et d'avoir le traitement implémenté dans une fonctionfn2(T[]): U[]
, qui est appelée beaucoup moins de fois que fn (et donc optimiser plus facilement le traitement). Aujourd'hui c'est fait avec de la codegen, et ce serait beaucoup plus ergonomique avec des génériques (compilation plus rapide que la codegen, pas besoin de déclarer la liste des variantes à générer, facile d'adapter le code, etc.)[^] # Re: Conteneurs
Posté par David Delassus (site web personnel) . Évalué à 3.
Sauf que je peux déjà plus ou moins le faire sans Generics ça.
Alors oui, ça fait un peu de boilerplate. Mais c'est possible.
Donc je persiste et signe, les generics servent surtout a faire de la composition d'interface justement pour retirer le boilerplate de ces fonctions.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Conteneurs
Posté par barmic 🦦 . Évalué à 4.
C'est pas un boilerplate c'est in cas d'erreur supprimé car vérifié par le compilateur. Ce n'est pas une question de verbosité mais de fiabilité.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.