A force je ne me souviens même plus quand j'en ai eu besoin et que ça m'avait manqué !
La plupart du temps je m'en suis sorti avec des closures, je crois qu'on appelle ça comme ça, un peu comme pour sort.Slice.
Au final de ne pas avoir de généricité je crois que ça m'a évité pas mal de codes difficiles à maintenir, que l'on regrette après coup, comme j'ai pu en avoir dans d'autres langages.
Je suis quand même impatient d'essayer !
Les anti-Go ont l'absence de généricité comme argument principal. Mais comme toi, je pense que c'est à double tranchant car sympa de prime abord mais pouvant rapidement rendre le code trop complexe…
Mais bon, cela a fini par être introduit et on croise les doigts pour que ce soit utilisé judicieusement. Je vais essayer ça dès que j'en ai le temps.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Posté par El Titi .
Évalué à 4.
Dernière modification le 16 décembre 2021 à 22:24.
Les anti-go ont surtout la gestion des erreurs en horreur, qui rappelle le vieux temps du C:
func myFunc() error {
err := foo()
if err != nil {
return err
} err = bar()
if err != nil {
return err
}
err = baz()
if err != nil {
return err
} return nil
}
Et ce n'est pas près de changer même avec le defer
En C, on peut, pour la gestion des erreurs, mettre en œuvre un mécanisme similaire aux exceptions grâce à la bibliothèque setjmp, et je ne m'en étais pas privé à l'époque…
Pour nous émanciper des géants du numérique : Zelbinium !
On peut aussi en Go avec panic/defer.
J'ai essayé pour voir, dans des cas où ça ne changeait pas grand chose mais j'en suis vite revenu.
Aujourd'hui c'est quand je me remet au Python que ça m'angoisse de ne pas annoter tout de suite une éventuelle erreur ni de la retourner explicitement !
je ne comprends pas les commentaires parent et grand parent.
je pense qu'il y a une mauvaise compréhension sur l'utilisation des génériques en Go.
En tant que dev normal, ça ne change strictement rien à ton code. Cela n'impacte en rien la complexité du code.
En tant de dev de librairie, ça peut au contraire réduire la compléxité du code et augmenter sa lisibilité. Surtout que tu ne seras pas obligé de l'utiliser, le code reste retrocompatible.
Donc les «pouvant rapidement rendre le code trop complexe» et autres «Au final de ne pas avoir de généricité je crois que ça m'a évité pas mal de codes difficiles à maintenir» je pense sincèrement qu'on ne parle pas de la même feature ou alors il faut donner des exemples car les génériques sont justement pour diminuer la complexité de certains codes
J'ai eu le cas inverse en réécrivant une lib Python perso fortement basée sur generic + héritage.
Une lib pour faire des tables de rapports pdf avec gestion des cumuls, ruptures, sauts de pages & co avec comme principe que chaque colonne peut contenir n'importe quel type d'objet. Une lib que j'utilise dans quasiment tous mes projets.
Un vrai casse tête à réécrire en Go donc, sans generic ni héritage.
Finalement je me suis basé entièrement sur des closures et composition.
Miraculeusement j'ai réussi à ce que ça tienne en moins de ligne de code mais surtout j'évite ainsi tous les effets de bords qu'on retrouve avec trop de generic et héritage et mon code est beaucoup plus facile à maintenir malgré le fait qu'il soit utilisé dans beaucoup de projets.
Aussi ma conclusion pour le moment c'est que le generic c'est bien pour des codes très réduits et dont les fonctionnalités ne doivent plus bouger mais sur des libs plus grosses, comme toute dépendance c'est très difficile à faire évoluer.
Et encore, même pour des codes réduits, en Go on a l'habitude de faire des petites boucles à tous les coins de rues, pas sûr qu'on y gagne à les remplacer par des fonctions…
J'avoue qu'il faudrait montrer du code pour voir de quoi on parle !
une lib Python perso fortement basée sur generic + héritage.
Tu ne peux pas avoir fait cela en python, du moins pas au sens des génériques tels qu'introduis dans golang, car ils n'ont de sens que dans un langage à typage statique (ce qui n'est pas le cas de python).
La fonctionnalité dont on parle consiste à rajouter aux langages des paramètres de types pour les fonctions. De même qu'avant, comme dans tout langage statiquement typé, il y avait des paramètres pour les valeurs contraintes par des types, il y a maintenant des paramètres de types contraints par des interfaces (paramètres de types qui pourront contraindre les paramètres de valeurs). Ce qui permet de lier, génériquement (c'est-à-dire indépendamment du type réellement instancié lors de l'appel de la fonction), les types d'entrée et de sortie des fonctions. Cela permet simplement d'appliquer le principe DRY (Don't Repeat Yourself), et je ne vois pas comment tu peux effectuer cela avec de simples clôtures.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
je ne vois pas comment tu peux effectuer cela avec de simples clôture
Au lieu de manipuler directement ta structure ou ton objet, tu enrobe chaque usage par une fermeture. Le code n'a plus de lien avec le type de l'objet capturé avec la fermeture, mais avec le comportement de la fermeture elle même. C'est une sorte de pattern adapteur.
Le code n'a plus de lien avec le type de l'objet capturé avec la fermeture, mais avec le comportement de la fermeture elle même.
J'aurais du préciser : sans perdre le lien entre le type d'entrée et le type de sortie. ;-)
Sinon ce que tu décris c'est tout simplement le fonctionnement des interfaces jusqu'alors : une interface c'est juste un dictionnaire de fermetures. D'ailleurs pourquoi passer par des fermetures à sa sauce quand le langage fournit nativement un tel mécanisme ?
Un cas d'exemple simple et générique impossible à rendre, au niveau des types, avec les interfaces (ou fermetures) : le tri d'un tableau. Quand on a un tableau sur un type ordonné (que le type soit ordonné cela s'exprime par le fait qu'il satisfait une certaine interface), alors on peut trier (par ordre croissant ou décroissant) ce tableau : le type de sortie dépend du type de l'entrée, en sortie on a un tableau sur le même type de données qu'en entrée.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
Sinon ce que tu décris c'est tout simplement le fonctionnement des interfaces jusqu'alors : une interface c'est juste un dictionnaire de fermetures. D'ailleurs pourquoi passer par des fermetures à sa sauce quand le langage fournit nativement un tel mécanisme ?
Je ne sais pas.
Un cas d'exemple simple et générique impossible à rendre, au niveau des types, avec les interfaces (ou fermetures) : le tri d'un tableau. Quand on a un tableau sur un type ordonné (que le type soit ordonné cela s'exprime par le fait qu'il satisfait une certaine interface), alors on peut trier (par ordre croissant ou décroissant) ce tableau : le type de sortie dépend du type de l'entrée, en sortie on a un tableau sur le même type de données qu'en entrée.
Ceux qui passent par cette solution (ça m'est arrivé par exemple parce qu'avec du typage nominal tu n'a pas toujours la possibilité de décrire le type qui te convient) :
soit n'ont pas besoin de ça
soit tu te crée que des méthodes et pas des fonctions. Tu modifie les paramètres que l'on te donne plutôt que de retourner quelque chose
Moi non plus. Ce qui me fait douter du fait que wilk comprenne bien ce qu'est le système de interfaces, ainsi que le manque que vient combler le système des génériques. L'idée derrière ce système étant de pouvoir retrouver, dans le système de types, le type de la variable capturée dans l'environnement des fermetures que constitue l'interface. Alors qu'avant il pouvait seulement le retrouver par un switch sur type dans le code (d'où le nombre important de code qui prennent un interface {} en entrée). Il pouvait réfléchir dans le code, mais non dans le système de types, la structure de l'environnement de leurs fermetures.
On voit bien, sur leur déclaration, que les deux méthodes Abs sont des fermetures : la première capture un MyFloat et la seconde un *Vertex. Les génériques permettent juste de donner un nom de variable au type de la valeur capturée pour l'utiliser dans la signature des fonctions.
soit n'ont pas besoin de ça
Ça fait un peu « dis moi ce dont tu as besoin, je te dirais comment t'en passer ».
soit tu te crée que des méthodes et pas des fonctions. Tu modifie les paramètres que l'on te donne plutôt que de retourner quelque chose.
Même le tri en place du tableau, je doute que ce soit possible (génériquement) en golang avec seulement des interfaces (pour la bonne raison qu'une fonction d'ordre est un opérateur binaire, ce qui n'est pas gérer par les interfaces de base).
Après, qu'il existe des contournements, en l'absence de généricité, pour résoudre les problèmes que l'on a, je n'en doute pas. Là où je suis plus sceptique, c'est qu'ils auront plutôt tendance à compliquer le code et non le simplifier.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
Ça fait un peu « dis moi ce dont tu as besoin, je te dirais comment t'en passer ».
Non c'est l'inverse, quand tu écris un programme pour ton besoin, tu ne cherche pas à résoudre la quadrature du cercle. Tu te place dans un prisme limité qui n'a pas vocation à généraliser autant que possible toute problématique.
Même le tri en place du tableau, je doute que ce soit possible (génériquement) en golang avec seulement des interfaces (pour la bonne raison qu'une fonction d'ordre est un opérateur binaire, ce qui n'est pas gérer par les interfaces de base).
C'est juste pas vérifié par le système de type. Ça n'est pas équivalent en terme de vérification, mais beaucoup de code fonctionnent comme ça. Et un certain nombre sont bien plus utilisé et apportent bien plus à leurs utilisateurs que tout ce que j'ai pu écrire donc je ne me permettrai pas de les juger.
Posté par Narmer .
Évalué à 1.
Dernière modification le 27 décembre 2021 à 23:44.
quand tu écris un programme pour ton besoin, tu ne cherche pas à résoudre la quadrature du cercle. Tu te place dans un prisme limité qui n'a pas vocation à généraliser autant que possible toute problématique.
L'objet des génériques n'est pas d'être utilisé quand tu codes un simple programme, plutôt quand tu codes une lib ou un framework pour d'autre dev.
Cela dit, il n'est pas interdit de se créer ses propres outils pour faire ses propres programmes. Tout le monde ne fait pas des programmes obligatoirement triviaux …
Exemple personnel, un programme ayant besoin d'une structure d'arbre (deux types paramètres Node et Edge) associé à ses fonctions comme un visiteur par exemple. Tu te vois mal écrire des algos pour tous les types possible … les callbacks de ton visiteur perdront en lisibilité.
C'est moins une question de trivialité que d'exhaustivité.
Le niveau d'abstraction que tu cherche n'est pas toujours le même. Plus tu es abstrait plus tu gère de cas, mais ça peut être au détriment de l'évolution (tu as moins d'hypothèse sur ton type d'entrée, tu sais moins de choses sur lui) ou de l'utilisabilité (ton utilisateur peu moins se baser sur les types pour comprendre ce que tu attends). Dans un programme, tu es seul utilisateur, tu peux généralement énumérer tous les cas d'usages. Ça change drastiquement la donne.
Plus tu es abstrait plus tu gère de cas, mais ça peut être au détriment de l'évolution
J'ai commencé ma carrière pro en 1999, au cours de cette carrière de developpeur, puis de concepteur, puis d'architecte logiciel, puis d'architecte SI je me suis rendu compte qu'il existait un type de developpeur qui n'appréhendait pas bien l'abstraction. Or l'abstraction n'est qu'un outil comme un autre dans l'arsenal du developpeur. Un outil logique qui certe demande plus de rigueur mais n'en demeure pas moins une variable qui permet de dépasser des seuils quand même assez avancés. J'ai par exemple un projet assez complexe d'un point de vue algorithme en go qui ne peut pas avancer tant que je n'ai pas les génériques en go, c'est un side project, je ne vais pas me faire chier à gérer les switch sur des types.
Ainsi ces developpeurs ont en horreur TOUTE abstraction et véhiculent des inexactitudes voires des choses fausses sur les abstractions ne voyant que les défauts d'une mauvaise utilisation sans savoir expliquer/voir pourquoi elles sont parfois necessaires. J'ai des souvenir assez précis de discussions assez épiques quand je proposais des formations sur la conception et sur le DDD, où certains developpeurs ne comprenais pas/rien et ne voyait jamais l'intérêt d'un cas expliqué et parlais souvent de "bon sens". C'est que du bon sens disaient ils.
Des personnes influentes comme "Joel On Software" ont d'ailleurs parlé avec justesse du concept de "Leaky Abstraction" [1] pour dénoncer à l'époque et mettre en garde contre les dérives potentiels d'une utilisation abusive et non maitrisée des abstractions. Mais ce constat s'applique aussi à d'autre pratique par exemple l'optimisation prématurée [2] qui va conduire à des problématiques inutiles et couteuses à résoudre.
Bref, quand tu dis que l'abstraction se fait au détriment de l'évolution ou de l'utilisabilité, je pense personnellement qu'à minima, on ne parle pas de la même chose car la création d'une abstraction n'à qu'un seul but détecter une situation problématique récurrente bien cadrée et déterminée pour proposer une solution ouverte et non enfermante.
L'utilisation des génériques n'a jamais empecher une quelconque évolution du code, au contraire elle ouvre à plus de possibilité avec juste un seul bout de code !
Nous sommes d'accord que nous devons utiliser les abstractions avec mesure, mais c'est le cas de tous les outils logiques qu'utilisent les devs. Ils existent un biais chez certains developpeurs qui ont tendances à catégoriser toutes les abstractions dans un même tiroir, souvent parce qu'ils ne les maitrisent pas et surtout ils ne voient pas un autre avantage aux abstractions, c'est qu'on partage à peu près tous le même espace mental : autrement dit on parle de tous de la même chose et ça devient un langage technique international. J'ai récemment participé à un échange avec deux developpeurs "anti-abstraction" qui essayaient de résoudre et d'expliquer à l'autre d'abord l'espace du problème ensuite une solution les seuls concepts qu'ils utilisent sont 'table', 'index', clés etrangère , clef primaire … très bas niveau , trop bas niveau pour des discussions de grooming … le PO est plus que perdu. c'est fini le temps où les programmeurs savaient faire la différence entre Objet métier, MCD et MPD . Pour ces devs tout est table de base de données … je m'arrête là désolé pour le pavé
Je ne sais pas si tu m'a classé dans les anti abstraction. Tout est une abstraction et ne pas décider de quel niveau d'abstraction on utilise ne veut pas dire que l'on en utilise pas juste que ce sera probablement mal fait.
Bref, quand tu dis que l'abstraction se fait au détriment de l'évolution ou de l'utilisabilité, je pense personnellement qu'à minima, on ne parle pas de la même chose car la création d'une abstraction n'à qu'un seul but détecter une situation problématique récurrente bien cadrée et déterminée pour proposer une solution ouverte et non enfermante.
Je pense qu'on parle de la même chose, mais qu'on ne le décrit pas de la même façon.
Je vais donner des exemples pour que ce que soit plus clair.
Un typage "trop" large
En java, la bibliothèque de collection décrit ArrayList ← List ← Collection ← Iterable.
L'important n'est pas tant l'utilité du code, mais que le paramètre titi peut ici être typé en Iterable. C'est l'interface la plus ouverte que tu peux faire à cette fonction. Mais tu ne pourra alors pas la rééacrire pour tirer partie des stream de java 8 et plus (ou alors il faut que tu crée ton stream à la main) si tu avais gardé une collection tu pourrais écrire quelque chose comme :
returntotos.stream().anyMatch(this::predicate);
Ce que je vois par là c'est que lorsque tu choisi tes types pour élargir l'utilisabilité, tu peux facilement te retrouver à retirer une propriété de ton contrat d'entrée qui n'a pas d'intérêt dans ton implémentation actuelle mais qui a tout de même son utilité.
L'utilisabilité d'un typage restrictif
L'autre direction c'est le classique des types cachés que tu peux trouver en ocaml par exemple. Tu peux définir un type encrypted qui est une string cachée (c'est à dire un alias sur le type string, mais qui n'est pas vu comme une string dans le système de type) et avoir des fonctions qui ne prennent que des données encrypted. Ça permet d'avoir des garanties supplémentaires, même si en soit ta fonction pourrait très bien prendre n'importe quelle chaîne de caractère.
J'obtiens une exception parce que les List produites par toList() sont immutables. Donc aujourd'hui les développeurs d'openjdk sont coincés avec une interface List qui se considère mutable alors qu'ils aimeraient avoir des list immutables (oui oui c'est moche).
Là je donne des exemples sur des structures de données, mais tu peux avoir la même chose sur ton type générique (si tu es habitué à java : les T extends Bidule).
Des personnes influentes comme "Joel On Software" ont d'ailleurs parlé avec justesse du concept de "Leaky Abstraction" [1] pour dénoncer à l'époque et mettre en garde contre les dérives potentiels d'une utilisation abusive et non maitrisée des abstractions. Mais ce constat s'applique aussi à d'autre pratique par exemple l'optimisation prématurée [2] qui va conduire à des problématiques inutiles et couteuses à résoudre.
Faut que je retrouve j'avais suivi une discussion intéressante sur twitter où quelqu'un donnait un autre terme de "prémature optimization" bien plus clair pour ne pas invalider toute forme de discussion sur les performances.
Plus tu es abstrait plus tu gère de cas, mais ça peut être au détriment de l'évolution
Pas vraiment, tu gères ce qu’il y a de commun entre plusieurs situations mais si il y rien de commun entre deux situations et que tu essayes de faire rentrer le tout au pied de biche dans une même abstraction … en principe ton abstraction ne va rien gérer du tout.
C’est là que tu risque si l’abstraction est en plus mal fichu de faire rentrer ton cas d’utilisation dans le cadre abstrait mal foutu. On pense à par exemple certains CMS qu’il faut contourner pour faire des choses qui rentrent pas dans le cadre.
La question que ça pose est le fait que l’abstraction ne doit pas trop s’imposer en tant que framework. Un bon exemple est une structure de donnée abstraite genre les tableaux ou les arbres, tout programme peut l’utiliser comme il l’entend comme n’importe quel autre type sans que ça n’engage à grand chose sur l’architecture du programme, et ça capture vraiment des choses qui sont communes à plein de situations.
Je comprend bien ce que sont les interfaces et les types génériques.
Mais le problème n'est pas tant que le code soit plus ou moins compliqué, ça dépend vraiment des cas, mais le fait qu'il crée une dépendance.
Si c'est un tri par exemple c'est bénéfique car une fois bien codé on n'y touchera plus, et surtout on n'ajoutera jamais de fonctionnalité.
En revanche si on passe à un niveau plus élevé, comme dans mon cas d'une lib qui génère un pdf, c'est bien plus délicat car il y a potentiellement des fonctionnalités qui vont s'ajouter et impacter tout le monde (problème classique des dépendances et de l'héritage).
Je ne vois pas de fermeture dans l'exemple que tu donnes par contre ?
J'en vois sur l'exemple de sort.Slice où la fonction less utilisera probablement un tableau en dehors.
L'intérêt des fermetures est de faire en sorte que l'algo central ne dépende d'aucun type particulier, même pas un type générique mais de la plus petite interface possible, par exemple func(i, j int) bool. Le code dépendant du type est dans la fonction avec fermeture et propre à chaque utilisation et donc en dehors de la lib.
Dans mon cas par exemple j'ai un tableau composé de lignes composées de cellules.
Il me faut faire un pdf de tout ça.
L'algo appelle chaque cellule et lui demande son rendu, pour ça une interface Rendu marche très bien. La où ça devient casse tête c'est quand une des cellules a besoin de ses cellules voisines pour calculer son rendu, par ex cellule 2 = cellule 0 + cellule 1
En Python la méthode Rendu de la cellule un = cellule.parent.cellules[0] + cellule.parent.cellules[1]
En Go tintin car cellule.parent.cellules[x] correspondra simplement à une cellule ayant l'interface Rendu. Il faudrait éventuellement faire un cast et perdre l'intérêt du typage statique, ce que je ne voulais pas.
Avec les fermetures la méthode rendu de ma cellule est appelée avec l'index de la ligne et c'est avec cet index que je vais chercher les cellules voisines (donc un tableau en dehors de ma méthode). Je n'ai même plus besoin de cellule.parent.
Bref, avec cette méthode de fermeture mon code de lib est réduit au strict minimum en manipulant uniquement des indexes, par rapport au Python où je pouvais me permettre de manipuler directement les lignes et cellules mais avec un code de lib du coup beaucoup plus complexe. Autrement dit je préfère dans certains cas un code de lib plus simple et déplacer ce qui est complexe au niveau de l'application. Parfois c'est l'inverse (tri par ex).
Avec le générique en Go il y aura moyen de se rapprocher de ce que l'on peut faire avec du typage dynamique. Avec par exemple un tableau qui contient des lignes de type T défini au niveau de l'application. C'est sûrement ce que j'aurai fait s'il y avait eu du générique à l'époque au lieu d'essayer la méthode avec fermeture que je préfère finalement pour ce cas de figure.
Je sais pas si je suis clair, c'est vraiment difficile à décrire…
Dans la première méthode Asb()f est capturé et v dans la seconde. C'est comme ça que le l'ai compris, même si je ne suis pas certains qu'on puisse parler de capture pour une référence qui est passée de cette façon à la méthode (comme le self de python).
L'intérêt des fermetures est de faire en sorte que l'algo central ne dépende d'aucun type particulier, même pas un type générique mais de la plus petite interface possible, par exemple func(i, j int) bool. Le code dépendant du type est dans la fonction avec fermeture et propre à chaque utilisation et donc en dehors de la lib.
Ça n'est pas clair pour moi. Tu semble parlait indistinctement de fermeture et d'interface.
// une interface typeRenduinterface{Rendu(i,jint)bool}// une lambdavarrendufunc(i,jint)bool
Les 2 manières peuvent faire de la capture pour calculer leur rendu comme tu le souhaite.
Dans ton exemple f et v ne sont pas capturés puisqu'ils sont passés en paramètres.
Les méthodes c'est exactement comme si on écrivait Abs(f MyFloat)
// une interface typeRenduinterface{Rendu(i,jint)string}
Oui, c'est exactement ce que je décrit, une interface qui utilise une fonction de fermeture. Ce qui est capturé c'est le tableau.
L'algo n'a pas à traiter le tableau, il va juste appeler les cellules par leur index.
Ceci à la place d'une interface, beaucoup plus classique qui serait :
En Python je redéfinissais la méthode Rendu de ma cellule et cette méthode accédait aux cellules voisines par quelque chose comme self.parent.cellules[i-1]...
En Go je pourrai avoir une interface pour récupérer les cellules voisines
la version closure fait le append avec ou sans l'erreur pas sûr que ce soit toujours une bonne idée
le slot d'appel est bien plus simple en générique (je ne serait pas surpris qu'il existe du sucre syntaxique pour ce genre de chose), la complexité ajouté dans la méthode générique ne me semble pas insurmontable
tu as moins de bruit avec la première variable retournée par la méthode non générique dont on ne sait pas trop quoi faire. La méthode générique a un usage plus courant avec un retour de la forme (réponse attendue, erreur)
Après ça n'est qu'une manière de faire, tu peux préférer avoir un itérateur qui va te permettre de t'arrêter là où tu souhaite (ou de commencer à faire des choses avant de tout charger et utiliser une coroutine pour ça va poser des questions de backpressure).
Je ne vois pas de fermeture dans l'exemple que tu donnes par contre ?
Les fonctions Abs sont des fermetures, une interface c'est un dictionnaire de fermetures. Prenons le code d'usage de l'interface :
funcmain(){varaAbserf:=MyFloat(-math.Sqrt2)v:=Vertex{3,4}a=f// a MyFloat implements Absera=&v// a *Vertex implements Abser// In the following line, v is a Vertex (not *Vertex)// and does NOT implement Abser.a=vfmt.Println(a.Abs())}
la variable a est un dictionnaire de fermeture et a.Absest une fermeture (la variable capturée étant soit un Myfloat, soit un *Vertex). Il en est de même pour n'importe quelle interface. C'est pareil en POO : un objet c'est un dictionnaire de fermetures qui partagent le même environnement (les variables d'instance). C'est un usage classique des fermetures pour faire du polymoprhisme ad-hoc, qui est celui que tu décris, avec de l'encapsulation. Cela permet d'utiliser, comme expliqué dans l'article que tu cites sur medium, le même algorithme sur différentes structures de données qui partagent un comportement commun (décrit par l'interface).
Voyons voir ce qu'est une fermeture (je l'écris en OCaml, c'est plus simple pour moi, surtout pour la suite).
letfooij=2*i+j
ici la fonction foo est dite close car toutes les variables qui apparaissent dans son code sont des paramètres formels de celle-ci. En revanche ce n'est plus le cas celle-ci:
leti=2letbarj=2*i+j
ici la variable i fait partie de l'environnement de bar (on dit que i apparaît libre dans bar, là où il est lié dans foo), cette dernière est une application partielle de foo: let bar = foo 2. La fermeture consiste à transformer bar en une paire constituée de la fonction closefoo et du paramètre i = 2 : une fonction non close est transformée en une fonction close, on la clôture ;-)
Reprenons maintenant l'interface Abser de l'exemple en go:
typeAbserinterface{Abs()float64}
En OCaml, on écrirait cela ainsi :
typeabser={abs:unit->float}
mais ce type sera habité par des fermetures, c'est à dire des paires dont la fonction close aura cette forme :
type'aabser_close={abs:'a->float}
et la clôture aura cette forme :
type'aabser_closure='aabser_close*'a
autrement dit une fonction sans variable libre sur une type 'a, ainsi qu'une valeur de ce même type 'a correspondant à celle qui est capturée et encapsulée dans la clôture. Maintenant le type de départ abser est équivalent à la réunion (ou somme) sur tous les types possibles des fermetures précédentes :
typeabser=Abser:'aabser_closure->abser
Géométriquement, on peut représenter cela par un cône:
La base circulaire représente tous les types possibles du langage et le sommet est justement le type abser. Ce cône peut être vu comme un graphe orienté étiqueté, où chaque arrête va de la base vers le sommet avec comme étiquette la fonction qui calcule la valeur absolue pour le type en question. Il illustre comment on transforme un Myfloat ou un *Vertex en un Abser. Maintenant, quand on veut utiliser un Abser, il faut retourner l'orientation du graphe : on ne va plus de la base vers le sommet, mais du sommet vers la base. Alors une valeur de type Abser ne peut être utilisée quand observant la valeur qu'elle encapsule, en redescendant le long de l'arrête correspondant et en appliquant la fonction en étiquette. Ce mécanisme d'utilisation d'un Abser est ce que les programmeurs appellent le dynamic dispatch, qui est au cœur de la POO et du polymorphisme ad-hoc.
La généricité, en revanche, permet d'appliquer le même algorithme sur un même structure de données qui est un conteneur, comme le sont les tableaux (on parle plutôt volontiers, dans ce cas, de polymorphisme structurel). Ici, vous vous en sortez avec des fermetures (comme pour la fonction de tri), parce qu'un tableau peut être vu comme un dictionnaire clef-valeur où les clefs sont des int. Ainsi, au lieu d'avoir une fonction avec un type polymorphe, qui prend une fonction de comparaison sur le type contenu dans le tableau ('a -> 'a > bool), vous pouvez vous contentez d'un type monomoprhe int -> int -> bool en encapsulant le tableau et en accédant aux valeurs par leur index. Mais cela réduit la généricité au tableaux, qui sont built-in, et cela ne permet pas de créer ses propres type génériques.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
Des lambdas ou fonctions anonymes plutôt ? Les clôtures c’est quand tu utilises dans ta fonction anonyme une variable définie à l’extérieur de la fonction.
Tu peux employer tout simplement le terme « fonction » alors j’imagine. Une fermeture (informatique) est juste un type particulier de fonction qui capture des variables de son scope de définition.
J’imagine qu’on peut considérer qu’une fonction habituelle est une clôture dégénérée qui ne capture pas de variable de l’environnement si on veut, mais Wikipédia en anglais fait plutôt l’inverse (cf. https://en.wikipedia.org/wiki/Closure_(computer_programming)#Anonymous_functions — « a closure is an instance of a function » )
# Trou de mémoire
Posté par wilk . Évalué à 4.
A force je ne me souviens même plus quand j'en ai eu besoin et que ça m'avait manqué !
La plupart du temps je m'en suis sorti avec des closures, je crois qu'on appelle ça comme ça, un peu comme pour sort.Slice.
Au final de ne pas avoir de généricité je crois que ça m'a évité pas mal de codes difficiles à maintenir, que l'on regrette après coup, comme j'ai pu en avoir dans d'autres langages.
Je suis quand même impatient d'essayer !
[^] # Re: Trou de mémoire
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
Les anti-Go ont l'absence de généricité comme argument principal. Mais comme toi, je pense que c'est à double tranchant car sympa de prime abord mais pouvant rapidement rendre le code trop complexe…
Mais bon, cela a fini par être introduit et on croise les doigts pour que ce soit utilisé judicieusement. Je vais essayer ça dès que j'en ai le temps.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: Trou de mémoire
Posté par El Titi . Évalué à 4. Dernière modification le 16 décembre 2021 à 22:24.
Les anti-go ont surtout la gestion des erreurs en horreur, qui rappelle le vieux temps du C:
err = bar()func myFunc() error {
err := foo()
if err != nil {
return err
}
if err != nil {
return err
}
return nilerr = baz()
if err != nil {
return err
}
}
Et ce n'est pas près de changer même avec le defer
Source: l'auteur de lazygit
[^] # Re: Trou de mémoire
Posté par Claude SIMON (site web personnel) . Évalué à 3.
En C, on peut, pour la gestion des erreurs, mettre en œuvre un mécanisme similaire aux exceptions grâce à la bibliothèque setjmp, et je ne m'en étais pas privé à l'époque…
Pour nous émanciper des géants du numérique : Zelbinium !
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 3.
On peut aussi en Go avec panic/defer.
J'ai essayé pour voir, dans des cas où ça ne changeait pas grand chose mais j'en suis vite revenu.
Aujourd'hui c'est quand je me remet au Python que ça m'angoisse de ne pas annoter tout de suite une éventuelle erreur ni de la retourner explicitement !
[^] # Re: Trou de mémoire
Posté par Narmer . Évalué à 5.
je ne comprends pas les commentaires parent et grand parent.
je pense qu'il y a une mauvaise compréhension sur l'utilisation des génériques en Go.
En tant que dev normal, ça ne change strictement rien à ton code. Cela n'impacte en rien la complexité du code.
En tant de dev de librairie, ça peut au contraire réduire la compléxité du code et augmenter sa lisibilité. Surtout que tu ne seras pas obligé de l'utiliser, le code reste retrocompatible.
Donc les «pouvant rapidement rendre le code trop complexe» et autres «Au final de ne pas avoir de généricité je crois que ça m'a évité pas mal de codes difficiles à maintenir» je pense sincèrement qu'on ne parle pas de la même feature ou alors il faut donner des exemples car les génériques sont justement pour diminuer la complexité de certains codes
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 3.
J'ai eu le cas inverse en réécrivant une lib Python perso fortement basée sur generic + héritage.
Une lib pour faire des tables de rapports pdf avec gestion des cumuls, ruptures, sauts de pages & co avec comme principe que chaque colonne peut contenir n'importe quel type d'objet. Une lib que j'utilise dans quasiment tous mes projets.
Un vrai casse tête à réécrire en Go donc, sans generic ni héritage.
Finalement je me suis basé entièrement sur des closures et composition.
Miraculeusement j'ai réussi à ce que ça tienne en moins de ligne de code mais surtout j'évite ainsi tous les effets de bords qu'on retrouve avec trop de generic et héritage et mon code est beaucoup plus facile à maintenir malgré le fait qu'il soit utilisé dans beaucoup de projets.
Aussi ma conclusion pour le moment c'est que le generic c'est bien pour des codes très réduits et dont les fonctionnalités ne doivent plus bouger mais sur des libs plus grosses, comme toute dépendance c'est très difficile à faire évoluer.
Et encore, même pour des codes réduits, en Go on a l'habitude de faire des petites boucles à tous les coins de rues, pas sûr qu'on y gagne à les remplacer par des fonctions…
J'avoue qu'il faudrait montrer du code pour voir de quoi on parle !
[^] # Re: Trou de mémoire
Posté par kantien . Évalué à 6.
Tu ne peux pas avoir fait cela en python, du moins pas au sens des génériques tels qu'introduis dans golang, car ils n'ont de sens que dans un langage à typage statique (ce qui n'est pas le cas de python).
La fonctionnalité dont on parle consiste à rajouter aux langages des paramètres de types pour les fonctions. De même qu'avant, comme dans tout langage statiquement typé, il y avait des paramètres pour les valeurs contraintes par des types, il y a maintenant des paramètres de types contraints par des interfaces (paramètres de types qui pourront contraindre les paramètres de valeurs). Ce qui permet de lier, génériquement (c'est-à-dire indépendamment du type réellement instancié lors de l'appel de la fonction), les types d'entrée et de sortie des fonctions. Cela permet simplement d'appliquer le principe DRY (Don't Repeat Yourself), et je ne vois pas comment tu peux effectuer cela avec de simples clôtures.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 4.
Au lieu de manipuler directement ta structure ou ton objet, tu enrobe chaque usage par une fermeture. Le code n'a plus de lien avec le type de l'objet capturé avec la fermeture, mais avec le comportement de la fermeture elle même. C'est une sorte de pattern adapteur.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par kantien . Évalué à 3.
J'aurais du préciser : sans perdre le lien entre le type d'entrée et le type de sortie. ;-)
Sinon ce que tu décris c'est tout simplement le fonctionnement des interfaces jusqu'alors : une interface c'est juste un dictionnaire de fermetures. D'ailleurs pourquoi passer par des fermetures à sa sauce quand le langage fournit nativement un tel mécanisme ?
Un cas d'exemple simple et générique impossible à rendre, au niveau des types, avec les interfaces (ou fermetures) : le tri d'un tableau. Quand on a un tableau sur un type ordonné (que le type soit ordonné cela s'exprime par le fait qu'il satisfait une certaine interface), alors on peut trier (par ordre croissant ou décroissant) ce tableau : le type de sortie dépend du type de l'entrée, en sortie on a un tableau sur le même type de données qu'en entrée.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 3.
Je ne sais pas.
Ceux qui passent par cette solution (ça m'est arrivé par exemple parce qu'avec du typage nominal tu n'a pas toujours la possibilité de décrire le type qui te convient) :
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par kantien . Évalué à 3.
Moi non plus. Ce qui me fait douter du fait que wilk comprenne bien ce qu'est le système de interfaces, ainsi que le manque que vient combler le système des génériques. L'idée derrière ce système étant de pouvoir retrouver, dans le système de types, le type de la variable capturée dans l'environnement des fermetures que constitue l'interface. Alors qu'avant il pouvait seulement le retrouver par un switch sur type dans le code (d'où le nombre important de code qui prennent un
interface {}
en entrée). Il pouvait réfléchir dans le code, mais non dans le système de types, la structure de l'environnement de leurs fermetures.Si on prend l'exemple de a tour of go:
On voit bien, sur leur déclaration, que les deux méthodes
Abs
sont des fermetures : la première capture unMyFloat
et la seconde un*Vertex
. Les génériques permettent juste de donner un nom de variable au type de la valeur capturée pour l'utiliser dans la signature des fonctions.Ça fait un peu « dis moi ce dont tu as besoin, je te dirais comment t'en passer ».
Même le tri en place du tableau, je doute que ce soit possible (génériquement) en golang avec seulement des interfaces (pour la bonne raison qu'une fonction d'ordre est un opérateur binaire, ce qui n'est pas gérer par les interfaces de base).
Après, qu'il existe des contournements, en l'absence de généricité, pour résoudre les problèmes que l'on a, je n'en doute pas. Là où je suis plus sceptique, c'est qu'ils auront plutôt tendance à compliquer le code et non le simplifier.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 3.
Non c'est l'inverse, quand tu écris un programme pour ton besoin, tu ne cherche pas à résoudre la quadrature du cercle. Tu te place dans un prisme limité qui n'a pas vocation à généraliser autant que possible toute problématique.
C'est juste pas vérifié par le système de type. Ça n'est pas équivalent en terme de vérification, mais beaucoup de code fonctionnent comme ça. Et un certain nombre sont bien plus utilisé et apportent bien plus à leurs utilisateurs que tout ce que j'ai pu écrire donc je ne me permettrai pas de les juger.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par Narmer . Évalué à 1. Dernière modification le 27 décembre 2021 à 23:44.
L'objet des génériques n'est pas d'être utilisé quand tu codes un simple programme, plutôt quand tu codes une lib ou un framework pour d'autre dev.
Cela dit, il n'est pas interdit de se créer ses propres outils pour faire ses propres programmes. Tout le monde ne fait pas des programmes obligatoirement triviaux …
Exemple personnel, un programme ayant besoin d'une structure d'arbre (deux types paramètres Node et Edge) associé à ses fonctions comme un visiteur par exemple. Tu te vois mal écrire des algos pour tous les types possible … les callbacks de ton visiteur perdront en lisibilité.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 3.
C'est moins une question de trivialité que d'exhaustivité.
Le niveau d'abstraction que tu cherche n'est pas toujours le même. Plus tu es abstrait plus tu gère de cas, mais ça peut être au détriment de l'évolution (tu as moins d'hypothèse sur ton type d'entrée, tu sais moins de choses sur lui) ou de l'utilisabilité (ton utilisateur peu moins se baser sur les types pour comprendre ce que tu attends). Dans un programme, tu es seul utilisateur, tu peux généralement énumérer tous les cas d'usages. Ça change drastiquement la donne.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par Narmer . Évalué à 1.
J'ai commencé ma carrière pro en 1999, au cours de cette carrière de developpeur, puis de concepteur, puis d'architecte logiciel, puis d'architecte SI je me suis rendu compte qu'il existait un type de developpeur qui n'appréhendait pas bien l'abstraction. Or l'abstraction n'est qu'un outil comme un autre dans l'arsenal du developpeur. Un outil logique qui certe demande plus de rigueur mais n'en demeure pas moins une variable qui permet de dépasser des seuils quand même assez avancés. J'ai par exemple un projet assez complexe d'un point de vue algorithme en go qui ne peut pas avancer tant que je n'ai pas les génériques en go, c'est un side project, je ne vais pas me faire chier à gérer les switch sur des types.
Ainsi ces developpeurs ont en horreur TOUTE abstraction et véhiculent des inexactitudes voires des choses fausses sur les abstractions ne voyant que les défauts d'une mauvaise utilisation sans savoir expliquer/voir pourquoi elles sont parfois necessaires. J'ai des souvenir assez précis de discussions assez épiques quand je proposais des formations sur la conception et sur le DDD, où certains developpeurs ne comprenais pas/rien et ne voyait jamais l'intérêt d'un cas expliqué et parlais souvent de "bon sens". C'est que du bon sens disaient ils.
Des personnes influentes comme "Joel On Software" ont d'ailleurs parlé avec justesse du concept de "Leaky Abstraction" [1] pour dénoncer à l'époque et mettre en garde contre les dérives potentiels d'une utilisation abusive et non maitrisée des abstractions. Mais ce constat s'applique aussi à d'autre pratique par exemple l'optimisation prématurée [2] qui va conduire à des problématiques inutiles et couteuses à résoudre.
Bref, quand tu dis que l'abstraction se fait au détriment de l'évolution ou de l'utilisabilité, je pense personnellement qu'à minima, on ne parle pas de la même chose car la création d'une abstraction n'à qu'un seul but détecter une situation problématique récurrente bien cadrée et déterminée pour proposer une solution ouverte et non enfermante.
L'utilisation des génériques n'a jamais empecher une quelconque évolution du code, au contraire elle ouvre à plus de possibilité avec juste un seul bout de code !
Nous sommes d'accord que nous devons utiliser les abstractions avec mesure, mais c'est le cas de tous les outils logiques qu'utilisent les devs. Ils existent un biais chez certains developpeurs qui ont tendances à catégoriser toutes les abstractions dans un même tiroir, souvent parce qu'ils ne les maitrisent pas et surtout ils ne voient pas un autre avantage aux abstractions, c'est qu'on partage à peu près tous le même espace mental : autrement dit on parle de tous de la même chose et ça devient un langage technique international. J'ai récemment participé à un échange avec deux developpeurs "anti-abstraction" qui essayaient de résoudre et d'expliquer à l'autre d'abord l'espace du problème ensuite une solution les seuls concepts qu'ils utilisent sont 'table', 'index', clés etrangère , clef primaire … très bas niveau , trop bas niveau pour des discussions de grooming … le PO est plus que perdu. c'est fini le temps où les programmeurs savaient faire la différence entre Objet métier, MCD et MPD . Pour ces devs tout est table de base de données … je m'arrête là désolé pour le pavé
[1] https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/
[2] https://effectiviology.com/premature-optimization/
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 3.
Je ne sais pas si tu m'a classé dans les anti abstraction. Tout est une abstraction et ne pas décider de quel niveau d'abstraction on utilise ne veut pas dire que l'on en utilise pas juste que ce sera probablement mal fait.
Je pense qu'on parle de la même chose, mais qu'on ne le décrit pas de la même façon.
Je vais donner des exemples pour que ce que soit plus clair.
Un typage "trop" large
En java, la bibliothèque de collection décrit ArrayList ← List ← Collection ← Iterable.
Si tu as une fonction comme ceci :
L'important n'est pas tant l'utilité du code, mais que le paramètre
titi
peut ici être typé en Iterable. C'est l'interface la plus ouverte que tu peux faire à cette fonction. Mais tu ne pourra alors pas la rééacrire pour tirer partie des stream de java 8 et plus (ou alors il faut que tu crée ton stream à la main) si tu avais gardé une collection tu pourrais écrire quelque chose comme :Ce que je vois par là c'est que lorsque tu choisi tes types pour élargir l'utilisabilité, tu peux facilement te retrouver à retirer une propriété de ton contrat d'entrée qui n'a pas d'intérêt dans ton implémentation actuelle mais qui a tout de même son utilité.
L'utilisabilité d'un typage restrictif
L'autre direction c'est le classique des types cachés que tu peux trouver en ocaml par exemple. Tu peux définir un type
encrypted
qui est une string cachée (c'est à dire un alias sur le type string, mais qui n'est pas vu comme une string dans le système de type) et avoir des fonctions qui ne prennent que des données encrypted. Ça permet d'avoir des garanties supplémentaires, même si en soit ta fonction pourrait très bien prendre n'importe quelle chaîne de caractère.Les problèmes d'un typage pas assez restrictif
Si j'écris en java:
J'obtiens une exception parce que les
List
produites partoList()
sont immutables. Donc aujourd'hui les développeurs d'openjdk sont coincés avec une interfaceList
qui se considère mutable alors qu'ils aimeraient avoir des list immutables (oui oui c'est moche).Là je donne des exemples sur des structures de données, mais tu peux avoir la même chose sur ton type générique (si tu es habitué à java : les
T extends Bidule
).Faut que je retrouve j'avais suivi une discussion intéressante sur twitter où quelqu'un donnait un autre terme de "prémature optimization" bien plus clair pour ne pas invalider toute forme de discussion sur les performances.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par Thomas Douillard . Évalué à 3.
Pas vraiment, tu gères ce qu’il y a de commun entre plusieurs situations mais si il y rien de commun entre deux situations et que tu essayes de faire rentrer le tout au pied de biche dans une même abstraction … en principe ton abstraction ne va rien gérer du tout.
C’est là que tu risque si l’abstraction est en plus mal fichu de faire rentrer ton cas d’utilisation dans le cadre abstrait mal foutu. On pense à par exemple certains CMS qu’il faut contourner pour faire des choses qui rentrent pas dans le cadre.
La question que ça pose est le fait que l’abstraction ne doit pas trop s’imposer en tant que framework. Un bon exemple est une structure de donnée abstraite genre les tableaux ou les arbres, tout programme peut l’utiliser comme il l’entend comme n’importe quel autre type sans que ça n’engage à grand chose sur l’architecture du programme, et ça capture vraiment des choses qui sont communes à plein de situations.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 2.
J'ai posté une grosse réponse juste au dessus en pensant à vos 2 commentaires :) Je te réponds en direct juste pour que tu ai la notification.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 2.
Je comprend bien ce que sont les interfaces et les types génériques.
Mais le problème n'est pas tant que le code soit plus ou moins compliqué, ça dépend vraiment des cas, mais le fait qu'il crée une dépendance.
Si c'est un tri par exemple c'est bénéfique car une fois bien codé on n'y touchera plus, et surtout on n'ajoutera jamais de fonctionnalité.
En revanche si on passe à un niveau plus élevé, comme dans mon cas d'une lib qui génère un pdf, c'est bien plus délicat car il y a potentiellement des fonctionnalités qui vont s'ajouter et impacter tout le monde (problème classique des dépendances et de l'héritage).
Je ne vois pas de fermeture dans l'exemple que tu donnes par contre ?
J'en vois sur l'exemple de sort.Slice où la fonction less utilisera probablement un tableau en dehors.
L'intérêt des fermetures est de faire en sorte que l'algo central ne dépende d'aucun type particulier, même pas un type générique mais de la plus petite interface possible, par exemple
func(i, j int) bool
. Le code dépendant du type est dans la fonction avec fermeture et propre à chaque utilisation et donc en dehors de la lib.Dans mon cas par exemple j'ai un tableau composé de lignes composées de cellules.
Il me faut faire un pdf de tout ça.
L'algo appelle chaque cellule et lui demande son rendu, pour ça une interface
Rendu
marche très bien. La où ça devient casse tête c'est quand une des cellules a besoin de ses cellules voisines pour calculer son rendu, par excellule 2 = cellule 0 + cellule 1
En Python la méthode Rendu de la cellule un =
cellule.parent.cellules[0] + cellule.parent.cellules[1]
En Go tintin car
cellule.parent.cellules[x]
correspondra simplement à une cellule ayant l'interfaceRendu
. Il faudrait éventuellement faire un cast et perdre l'intérêt du typage statique, ce que je ne voulais pas.Avec les fermetures la méthode rendu de ma cellule est appelée avec l'index de la ligne et c'est avec cet index que je vais chercher les cellules voisines (donc un tableau en dehors de ma méthode). Je n'ai même plus besoin de
cellule.parent
.Bref, avec cette méthode de fermeture mon code de lib est réduit au strict minimum en manipulant uniquement des indexes, par rapport au Python où je pouvais me permettre de manipuler directement les lignes et cellules mais avec un code de lib du coup beaucoup plus complexe. Autrement dit je préfère dans certains cas un code de lib plus simple et déplacer ce qui est complexe au niveau de l'application. Parfois c'est l'inverse (tri par ex).
Avec le générique en Go il y aura moyen de se rapprocher de ce que l'on peut faire avec du typage dynamique. Avec par exemple un tableau qui contient des lignes de type T défini au niveau de l'application. C'est sûrement ce que j'aurai fait s'il y avait eu du générique à l'époque au lieu d'essayer la méthode avec fermeture que je préfère finalement pour ce cas de figure.
Je sais pas si je suis clair, c'est vraiment difficile à décrire…
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 2.
Dans la première méthode
Asb()
f
est capturé etv
dans la seconde. C'est comme ça que le l'ai compris, même si je ne suis pas certains qu'on puisse parler de capture pour une référence qui est passée de cette façon à la méthode (comme leself
de python).Ça n'est pas clair pour moi. Tu semble parlait indistinctement de fermeture et d'interface.
Les 2 manières peuvent faire de la capture pour calculer leur rendu comme tu le souhaite.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 2.
Dans ton exemple
f
etv
ne sont pas capturés puisqu'ils sont passés en paramètres.Les méthodes c'est exactement comme si on écrivait
Abs(f MyFloat)
Oui, c'est exactement ce que je décrit, une interface qui utilise une fonction de fermeture. Ce qui est capturé c'est le tableau.
L'algo n'a pas à traiter le tableau, il va juste appeler les cellules par leur index.
Ceci à la place d'une interface, beaucoup plus classique qui serait :
En Python je redéfinissais la méthode Rendu de ma cellule et cette méthode accédait aux cellules voisines par quelque chose comme
self.parent.cellules[i-1]...
En Go je pourrai avoir une interface pour récupérer les cellules voisines
Mais du coup je perdrais mon type de cellule… C'est là qu'avec des casts ou du generique ce serait faisable mais au final beaucoup plus compliqué.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 2.
Je pense que @kantien et moi avions compris que tu utilisais des méthodes anonymes :
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 2.
Oui mais ce n'est pas tellement le sujet.
Je passe la main à quelqu'un qui l'explique sûrement mieux que moi :
https://medium.com/capital-one-tech/closures-are-the-generics-for-go-cb32021fb5b5
Mais la question qui restera maintenant qu'on a du générique en Go c'est à quel moment préférer des fermetures ou du générique.
[^] # Re: Trou de mémoire
Posté par barmic 🦦 . Évalué à 3.
Hum l'article propose ça:
et à vu de nez la version générique serait ça:
Et du coup je dirais :
Après ça n'est qu'une manière de faire, tu peux préférer avoir un itérateur qui va te permettre de t'arrêter là où tu souhaite (ou de commencer à faire des choses avant de tout charger et utiliser une coroutine pour ça va poser des questions de backpressure).
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Trou de mémoire
Posté par kantien . Évalué à 3.
Les fonctions
Abs
sont des fermetures, une interface c'est un dictionnaire de fermetures. Prenons le code d'usage de l'interface :la variable
a
est un dictionnaire de fermeture eta.Abs
est une fermeture (la variable capturée étant soit unMyfloat
, soit un*Vertex
). Il en est de même pour n'importe quelle interface. C'est pareil en POO : un objet c'est un dictionnaire de fermetures qui partagent le même environnement (les variables d'instance). C'est un usage classique des fermetures pour faire du polymoprhisme ad-hoc, qui est celui que tu décris, avec de l'encapsulation. Cela permet d'utiliser, comme expliqué dans l'article que tu cites sur medium, le même algorithme sur différentes structures de données qui partagent un comportement commun (décrit par l'interface).Voyons voir ce qu'est une fermeture (je l'écris en OCaml, c'est plus simple pour moi, surtout pour la suite).
ici la fonction
foo
est dite close car toutes les variables qui apparaissent dans son code sont des paramètres formels de celle-ci. En revanche ce n'est plus le cas celle-ci:ici la variable
i
fait partie de l'environnement debar
(on dit quei
apparaît libre dansbar
, là où il est lié dansfoo
), cette dernière est une application partielle defoo
:let bar = foo 2
. La fermeture consiste à transformerbar
en une paire constituée de la fonction closefoo
et du paramètrei = 2
: une fonction non close est transformée en une fonction close, on la clôture ;-)Reprenons maintenant l'interface
Abser
de l'exemple en go:En OCaml, on écrirait cela ainsi :
mais ce type sera habité par des fermetures, c'est à dire des paires dont la fonction close aura cette forme :
et la clôture aura cette forme :
autrement dit une fonction sans variable libre sur une type
'a
, ainsi qu'une valeur de ce même type'a
correspondant à celle qui est capturée et encapsulée dans la clôture. Maintenant le type de départabser
est équivalent à la réunion (ou somme) sur tous les types possibles des fermetures précédentes :Géométriquement, on peut représenter cela par un cône:
La base circulaire représente tous les types possibles du langage et le sommet est justement le type
abser
. Ce cône peut être vu comme un graphe orienté étiqueté, où chaque arrête va de la base vers le sommet avec comme étiquette la fonction qui calcule la valeur absolue pour le type en question. Il illustre comment on transforme unMyfloat
ou un*Vertex
en unAbser
. Maintenant, quand on veut utiliser unAbser
, il faut retourner l'orientation du graphe : on ne va plus de la base vers le sommet, mais du sommet vers la base. Alors une valeur de typeAbser
ne peut être utilisée quand observant la valeur qu'elle encapsule, en redescendant le long de l'arrête correspondant et en appliquant la fonction en étiquette. Ce mécanisme d'utilisation d'unAbser
est ce que les programmeurs appellent le dynamic dispatch, qui est au cœur de la POO et du polymorphisme ad-hoc.La généricité, en revanche, permet d'appliquer le même algorithme sur un même structure de données qui est un conteneur, comme le sont les tableaux (on parle plutôt volontiers, dans ce cas, de polymorphisme structurel). Ici, vous vous en sortez avec des fermetures (comme pour la fonction de tri), parce qu'un tableau peut être vu comme un dictionnaire clef-valeur où les clefs sont des
int
. Ainsi, au lieu d'avoir une fonction avec un type polymorphe, qui prend une fonction de comparaison sur le type contenu dans le tableau ('a -> 'a > bool
), vous pouvez vous contentez d'un type monomoprheint -> int -> bool
en encapsulant le tableau et en accédant aux valeurs par leur index. Mais cela réduit la généricité au tableaux, qui sont built-in, et cela ne permet pas de créer ses propres type génériques.Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Trou de mémoire
Posté par Thomas Douillard . Évalué à 3.
Des lambdas ou fonctions anonymes plutôt ? Les clôtures c’est quand tu utilises dans ta fonction anonyme une variable définie à l’extérieur de la fonction.
[^] # Re: Trou de mémoire
Posté par wilk . Évalué à 3.
Il me semble que dans l'exemple slice.Sort c'est bien le système de clôture/closure, la fonction n'est pas forcément anonyme.
[^] # Re: Trou de mémoire
Posté par Thomas Douillard . Évalué à 2.
Tu peux employer tout simplement le terme « fonction » alors j’imagine. Une fermeture (informatique) est juste un type particulier de fonction qui capture des variables de son scope de définition.
J’imagine qu’on peut considérer qu’une fonction habituelle est une clôture dégénérée qui ne capture pas de variable de l’environnement si on veut, mais Wikipédia en anglais fait plutôt l’inverse (cf. https://en.wikipedia.org/wiki/Closure_(computer_programming)#Anonymous_functions — « a closure is an instance of a function » )
# Fuzzing
Posté par plouc . Évalué à 1.
Cette version intègre des tests de fuzzing. Ça doit être intéressant à essayer.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.