For the complete documentation index, see llms.txt. This page is also available as Markdown.

Revisiter les arrays et les slices à l'aide des Génériques

Le code pour ce chapitre est une continuation des Tableaux et Slices, disponible ici

Examinez à la fois SumAll et SumAllTails que nous avons écrits dans tableaux et slices. Si vous n'avez pas votre version, veuillez copier le code du chapitre tableaux et slices ainsi que les tests.

// Sum calcule le total à partir d'une slice de nombres.
func Sum(numbers []int) int {
	var sum int
	for _, number := range numbers {
		sum += number
	}
	return sum
}

// SumAllTails calcule les sommes de tous les nombres sauf le premier, étant donné une collection de slices.
func SumAllTails(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		if len(numbers) == 0 {
			sums = append(sums, 0)
		} else {
			tail := numbers[1:]
			sums = append(sums, Sum(tail))
		}
	}

	return sums
}

Voyez-vous un motif récurrent ?

  • Créer une sorte de valeur de résultat "initiale".

  • Itérer sur la collection, en appliquant une sorte d'opération (ou fonction) au résultat et à l'élément suivant dans la slice, en définissant une nouvelle valeur pour le résultat

  • Retourner le résultat.

Cette idée est couramment évoquée dans les cercles de programmation fonctionnelle, souvent appelée 'reduce' ou fold.

En programmation fonctionnelle, fold (également appelé reduce, accumulate, aggregate, compress, ou inject) fait référence à une famille de fonctions d'ordre supérieur qui analysent une structure de données récursive et, par l'utilisation d'une opération de combinaison donnée, recombinent les résultats du traitement récursif de ses parties constitutives, construisant une valeur de retour. Typiquement, un fold est présenté avec une fonction de combinaison, un nœud supérieur d'une structure de données, et éventuellement certaines valeurs par défaut à utiliser dans certaines conditions. Le fold procède ensuite à combiner des éléments de la hiérarchie de la structure de données, en utilisant la fonction de manière systématique.

Go a toujours eu des fonctions d'ordre supérieur, et depuis la version 1.18, il dispose également de génériques, il est donc maintenant possible de définir certaines de ces fonctions dont on parle dans notre domaine plus large. Il n'y a aucune raison de se cacher la tête dans le sable, c'est une abstraction très courante en dehors de l'écosystème Go et il sera bénéfique de la comprendre.

Maintenant, je sais que certains d'entre vous grimacent probablement.

Go est censé être simple

Ne confondez pas facilité et simplicité. Faire des boucles et copier-coller du code est facile, mais ce n'est pas nécessairement simple. Pour en savoir plus sur la simplicité et la facilité, regardez le chef-d'œuvre de Rich Hickey - Simple Made Easy.

Ne confondez pas non-familiarité et complexité. Fold/reduce peut initialement sembler effrayant et informatique, mais ce n'est en réalité qu'une abstraction d'une opération très courante. Prendre une collection et la combiner en un seul élément. Quand vous prenez du recul, vous réaliserez que vous faites probablement cela très souvent.

Un refactoring générique

Une erreur que les gens font souvent avec les nouvelles fonctionnalités de langage est qu'ils commencent par les utiliser sans avoir de cas d'utilisation concret. Ils s'appuient sur des conjectures et des suppositions pour guider leurs efforts.

Heureusement, nous avons écrit nos fonctions "utiles" et avons des tests autour d'elles, donc nous sommes maintenant libres d'expérimenter des idées dans la phase de refactoring du TDD et de savoir que quoi que nous essayions, il y a une vérification de sa valeur via nos tests unitaires.

Utiliser les génériques comme outil pour simplifier le code via l'étape de refactoring est beaucoup plus susceptible de vous guider vers des améliorations utiles, plutôt que des abstractions prématurées.

Nous sommes libres d'essayer des choses, de relancer nos tests, si nous aimons le changement, nous pouvons le valider. Sinon, il suffit d'annuler le changement. Cette liberté d'expérimenter est l'une des valeurs vraiment énormes du TDD.

Vous devriez être familier avec la syntaxe des génériques du chapitre précédent, essayez d'écrire votre propre fonction Reduce et utilisez-la dans Sum et SumAllTails.

Indices

Si vous réfléchissez d'abord aux arguments de votre fonction, cela vous donnera un très petit ensemble de solutions valides

  • Le tableau que vous souhaitez réduire

  • Une sorte de fonction de combinaison

"Reduce" est un modèle incroyablement bien documenté, il n'est pas nécessaire de réinventer la roue. Lisez le wiki, en particulier la section sur les listes, cela devrait vous suggérer un autre argument dont vous aurez besoin.

En pratique, il est pratique et naturel d'avoir une valeur initiale

Ma première version de Reduce

Reduce capture l'essence du modèle, c'est une fonction qui prend une collection, une fonction d'accumulation, une valeur initiale, et renvoie une seule valeur. Il n'y a pas de distractions désordonnées autour des types concrets.

Si vous comprenez la syntaxe des génériques, vous ne devriez avoir aucun problème à comprendre ce que fait cette fonction. En utilisant le terme reconnu Reduce, les programmeurs d'autres langages comprennent également l'intention.

L'utilisation

Sum et SumAllTails décrivent maintenant le comportement de leurs calculs comme les fonctions déclarées sur leurs premières lignes respectives. L'acte d'exécuter le calcul sur la collection est abstrait dans Reduce.

Autres applications de reduce

En utilisant des tests, nous pouvons jouer avec notre fonction reduce pour voir à quel point elle est réutilisable. J'ai copié nos fonctions d'assertion génériques du chapitre précédent.

La valeur zéro

Dans l'exemple de multiplication, nous montrons la raison d'avoir une valeur par défaut comme argument pour Reduce. Si nous nous appuyions sur la valeur par défaut de Go de 0 pour int, nous multiplierions notre valeur initiale par 0, puis les suivantes, donc vous n'obtiendriez jamais que 0. En la définissant à 1, le premier élément de la slice restera le même, et le reste se multipliera par les éléments suivants.

Si vous souhaitez paraître intelligent avec vos amis nerds, vous appelleriez cela L'élément neutre.

En mathématiques, un élément neutre, ou élément identité, d'une opération binaire opérant sur un ensemble est un élément de l'ensemble qui laisse inchangé tout élément de l'ensemble lorsque l'opération est appliquée.

Dans l'addition, l'élément neutre est 0.

1 + 0 = 1

Avec la multiplication, c'est 1.

1 * 1 = 1

Et si nous voulons réduire dans un type différent de A ?

Supposons que nous ayons une liste de transactions Transaction et que nous voulions une fonction qui les prendrait, plus un nom pour calculer leur solde bancaire.

Suivons le processus TDD.

Écrire le test d'abord

Essayer d'exécuter le test

Écrire le minimum de code pour que le test s'exécute et vérifier la sortie du test en échec

Nous n'avons pas encore nos types ou fonctions, ajoutons-les pour faire fonctionner le test.

Lorsque vous exécutez le test, vous devriez voir ce qui suit :

Écrire suffisamment de code pour le faire passer

Écrivons d'abord le code comme si nous n'avions pas de fonction Reduce.

Refactoring

À ce stade, ayez une discipline de contrôle de source et validez votre travail. Nous avons un logiciel fonctionnel, prêt à défier Monzo, Barclays, et al.

Maintenant que notre travail est validé, nous sommes libres de jouer avec, et d'essayer des idées différentes dans la phase de refactoring. Pour être honnête, le code que nous avons n'est pas vraiment mauvais, mais pour les besoins de cet exercice, je veux démontrer le même code en utilisant Reduce.

Mais cela ne compilera pas.

La raison est que nous essayons de réduire à un type différent du type de la collection. Cela semble effrayant, mais en réalité, il suffit d'ajuster la signature de type de Reduce pour que cela fonctionne. Nous n'aurons pas à changer le corps de la fonction, et nous n'aurons pas à changer nos appelants existants.

Nous avons ajouté une seconde contrainte de type qui nous a permis d'assouplir les contraintes sur Reduce. Cela permet aux gens de Reduce d'une collection de A en un B. Dans notre cas, de Transaction à float64.

Cela rend Reduce plus polyvalent et réutilisable, tout en restant sûr au niveau des types. Si vous essayez de relancer les tests, ils devraient compiler et passer.

Extension de la banque

Pour le plaisir, j'ai voulu améliorer l'ergonomie du code bancaire. J'ai omis le processus TDD par souci de brièveté.

Et voici le code mis à jour

Je pense que cela montre vraiment la puissance de l'utilisation de concepts comme Reduce. Le NewBalanceFor semble plus déclaratif, décrivant ce qui se passe, plutôt que comment. Souvent, lorsque nous lisons du code, nous parcourons de nombreux fichiers, et nous essayons de comprendre ce qui se passe, plutôt que comment, et ce style de code facilite bien cela.

Si je souhaite approfondir les détails, je peux, et je peux voir la logique métier de applyTransaction sans me soucier des boucles et de l'état changeant; Reduce s'en occupe séparément.

Fold/reduce sont assez universels

Les possibilités sont infinies™️ avec Reduce (ou Fold). C'est un modèle commun pour une raison, ce n'est pas seulement pour l'arithmétique ou la concaténation de chaînes. Essayez quelques autres applications.

  • Pourquoi ne pas mélanger certains color.RGBA en une seule couleur ?

  • Totaliser le nombre de votes dans un sondage, ou d'articles dans un panier d'achat.

  • Plus ou moins tout ce qui implique le traitement d'une liste.

Find

Maintenant que Go a des génériques, en les combinant avec des fonctions d'ordre supérieur, nous pouvons réduire beaucoup de code passe-partout dans nos projets, pour aider nos systèmes à être plus faciles à comprendre et à gérer.

Vous n'avez plus besoin d'écrire des fonctions Find spécifiques pour chaque type de collection que vous souhaitez rechercher, réutilisez plutôt ou écrivez une fonction Find. Si vous avez compris la fonction Reduce ci-dessus, écrire une fonction Find sera trivial.

Voici un test

Et voici l'implémentation

Encore une fois, comme elle prend un type générique, nous pouvons la réutiliser de nombreuses façons

Comme vous pouvez le voir, ce code est sans faille.

Conclusion

Lorsqu'elles sont utilisées avec goût, les fonctions d'ordre supérieur comme celles-ci rendront votre code plus simple à lire et à maintenir, mais rappelez-vous la règle empirique :

Utilisez le processus TDD pour développer un comportement réel et spécifique dont vous avez réellement besoin, dans la phase de refactoring, vous pourriez alors découvrir des abstractions utiles pour aider à nettoyer le code.

Pratiquez la combinaison du TDD avec de bonnes habitudes de contrôle de source. Validez votre travail lorsque votre test passe, avant d'essayer de refactoriser. De cette façon, si vous faites un gâchis, vous pouvez facilement revenir à votre état de fonctionnement.

Les noms sont importants

Faites un effort pour faire des recherches en dehors de Go, afin de ne pas réinventer des modèles qui existent déjà avec un nom déjà établi.

Écrire une fonction qui prend une collection de A et les convertit en B ? Ne l'appelez pas Convert, c'est Map. Utiliser le nom "propre" pour ces éléments réduira la charge cognitive pour les autres et rendra plus facile la recherche pour en apprendre davantage.

Cela ne semble pas idiomatique ?

Essayez d'avoir l'esprit ouvert.

Bien que les idiomes de Go ne changeront pas, et ne devraient pas changer radicalement en raison de la sortie des génériques, les idiomes vont changer - en raison du changement de langage ! Ce ne devrait pas être un point controversé.

Dire

Ce n'est pas idiomatique

Sans plus de détails, n'est pas une chose actionnable ou utile à dire. Surtout lorsqu'on discute de nouvelles fonctionnalités de langage.

Discutez avec vos collègues des modèles et du style de code en fonction de leurs mérites plutôt que du dogme. Tant que vous avez des tests bien conçus, vous pourrez toujours refactoriser et faire évoluer les choses au fur et à mesure que vous comprendrez ce qui fonctionne bien pour vous et votre équipe.

Ressources

Fold est un véritable fondamental en informatique. Voici quelques ressources intéressantes si vous souhaitez en savoir plus

Mis à jour