Aller au contenu
learn

5. Interfaces et génériques

Code : examples/l05_interfaces.v et examples/l05_generics.v — v run examples/l05_generics.v depuis code/v-for-csharp-java.

Nouvelles données : les projets d’une solution .NET

Section intitulée « Nouvelles données : les projets d’une solution .NET »

À partir de cette leçon, les exemples lisent les projets .NET de GuitarAlchemist/ga, une application de théorie musicale, au commit a26a7893. Le cours LadybugDB les a extraits des fichiers .csproj et .fsproj ; data/ga contient une copie de ses trois fichiers CSV :

Fichier Lignes Colonnes
projects.csv 111 path,name,language,sdk,frameworks,in_solution
project_refs.csv 266 from,to : une ProjectReference, du chemin d’un projet au chemin d’un projet
package_refs.csv 478 project,package,version : une PackageReference

Aucun champ ne contient de virgule, donc line.split(',') suffit ici, contrairement aux titres de pages de la leçon 3.

C# Java V
Un type implémente une interface en la déclarant : class Package : INamed en la déclarant : implements Named en ayant ses méthodes et ses champs
La déclaration obligatoire obligatoire facultative : struct Package implements Named
Membres méthodes, propriétés, implémentations par défaut méthodes, méthodes par défaut méthodes et champs
Tester le type if (n is Project p) if (n instanceof Project p) if n is Project { }

Source : Interfaces.

Une interface liste des méthodes, et aussi des champs. Project et Package ont tous deux un champ name et une méthode kind(), donc tous deux sont des Named, sans que leurs déclarations en disent un mot :

interface Named {
name string
kind() string
}
fn (p Project) kind() string {
return 'project'
}
fn (p Package) kind() string {
return 'package'
}
fn describe(n Named) string {
return '${n.kind()} ${n.name}'
}

Un tableau de valeurs d’interface contient des types différents, comme le ferait une List<INamed>. is teste le type et convertit la variable à l’intérieur du if, comme les types d’erreur de la leçon 3 :

items := [Named(project), package, Language{'F#'}]
for item in items {
println(describe(item))
println(' ${item.label()}')
if item is Project {
println(' sdk: ${item.sdk}')
}
}
project AllProjects.AppHost
[project] AllProjects.AppHost
sdk: Microsoft.NET.Sdk
package Aspire.Dashboard.Sdk.win-x64
[package] Aspire.Dashboard.Sdk.win-x64
language F#
[language] F#

Named(project) convertit le premier élément ; V convertit ensuite les autres vers le type d’élément du tableau.

Une méthode manquante est signalée là où la valeur est convertie

Section intitulée « Une méthode manquante est signalée là où la valeur est convertie »

C# signale un membre manquant sur la déclaration de la classe (error CS0535: 'Package' does not implement interface member 'INamed.Kind()'), et javac aussi (Package is not abstract and does not override abstract method kind() in Named). En V, Package ne déclare rien, donc l’erreur apparaît là où un Package devient un Named :

struct Package {
name string
version string
}
fn main() {
println(describe(Package{'Aspire.Hosting.AppHost', '9.5.1'}))
}
e05_missing_method.v:16:19: error: `Package` doesn't implement method `kind` of interface `Named`
14 |
15 | fn main() {
16 | println(describe(Package{'Aspire.Hosting.AppHost', '9.5.1'}))
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 | }

Une struct qui n’est jamais convertie n’est jamais vérifiée. Le implements facultatif déplace la vérification vers la déclaration, comme en C# et en Java :

struct Package implements Named {
name string
version string
}
e05_implements.v:6:1: error: `Package` doesn't implement method `kind` of interface `Named`
4 | }
5 |
6 | struct Package implements Named {
| ~~~~~~~~~~~~~~
7 | name string
8 | version string

Le Language de l’exemple l’utilise.

Les méthodes sur l’interface ne sont pas des implémentations par défaut

Section intitulée « Les méthodes sur l’interface ne sont pas des implémentations par défaut »

fn (n Named) label() déclare une méthode sur l’interface elle-même. Toute valeur Named la possède, et les types ne l’implémentent pas. La documentation avertit que ce n’est « PAS une “implémentation par défaut” comme en C# » : si Project avait son propre label(), appeler label() sur un Named exécuterait quand même la méthode de l’interface, et celle de Project seulement après is Project. Elle se comporte comme une méthode d’extension C# sur INamed, écrite comme une méthode.

Une méthode qui modifie son receveur va dans la section mut: de l’interface. Counter et Distinct comptent les références de paquets de deux façons :

interface Tally {
mut:
add(key string)
total() int
}
lines := os.read_lines('data/ga/package_refs.csv')![1..]
mut tallies := [Tally(Counter{}), Distinct{}]
for mut t in tallies {
for line in lines {
t.add(line.split(',')[1])
}
}
println('package references: ${tallies[0].total()}, distinct packages: ${tallies[1].total()}')
package references: 478, distinct packages: 136

for mut t est nécessaire pour appeler add. Hors de mut:, une méthode d’interface est appelée sur une valeur immuable, et un type dont la méthode a un receveur mut ne l’implémente pas :

interface Tally {
add(key string)
}
fn (mut c Counter) add(_ string) {
c.n++
}
fn main() {
t := Tally(Counter{})
t.add('Aspire.Hosting.AppHost')
}
e05_mut_receiver.v:15:7: error: `Counter` incorrectly implements method `add` of interface `Tally`: expected `Tally` which is immutable, not `mut &Counter`
13 |
14 | fn main() {
15 | t := Tally(Counter{})
| ~~~~~~~~~~~~~~~~
16 | t.add('Aspire.Hosting.AppHost')
17 | }
Details: main.Tally has `fn add(x main.Tally, key string)`
main.Counter has `fn add(mut c main.Counter, _ string)`
e05_mut_receiver.v:15:7: error: `Counter` does not implement interface `Tally`, cannot cast `Counter` to interface `Tally`
13 |
14 | fn main() {
15 | t := Tally(Counter{})
| ~~~~~~~~~~~~~~~~
16 | t.add('Aspire.Hosting.AppHost')
17 | }

L’inverse est accepté : total() est dans la section mut:, et Counter l’implémente avec un receveur immuable. La documentation de la 0.5.2 dit le contraire (un type avec fn (s MyStruct) write « implémente l’interface Foo, mais pas l’interface Bar », dont le write est dans mut:) ; le compilateur ne rejette qu’un receveur mut pour une méthode hors de mut: (voir le journal).

mut counter := Counter{}
mut copied := Tally(counter)
copied.add('Aspire.Hosting.AppHost')
println('copied: counter.n = ${counter.n}, copied.total() = ${copied.total()}')
mut shared_counter := &Counter{}
mut through_ref := Tally(shared_counter)
through_ref.add('Aspire.Hosting.AppHost')
println('reference: shared_counter.n = ${shared_counter.n}')
copied: counter.n = 0, copied.total() = 1
reference: shared_counter.n = 1

Convertir une valeur struct en interface la copie, comme le boxing d’une struct C# : avec struct Counter : ITally, ITally copied = counter; laisse aussi counter.N à 0, ce que j’ai vérifié avec .NET 11. Convertir une référence (&Counter) partage la struct, comme convertir une classe C# ou n’importe quel objet Java.

Les interfaces peuvent aussi incorporer d’autres interfaces, comme les structs incorporent des structs : interface ReaderWriter { Reader Writer } (Embedded interface).

C# Java V
Fonction générique Dictionary<string, int> CountBy<T>(…) <T> Map<String, Integer> countBy(…) fn count_by[T](…) map[string]int
Arguments de type inférés, ou CountBy<Project>(…) inférés, ou Util.<Project>countBy(…) inférés, ou count_by[Project](…)
Contraintes where T : IComparable<T> <T extends Comparable<T>> aucune : chaque usage est vérifié avec le type concret
À l’exécution une version par type valeur, du code partagé pour les références effacés en Object ou en la borne une copie de la fonction par type utilisé

Source : Generics.

count_by regroupe des éléments par clé, comme GroupBy(key).ToDictionary(g => g.Key, g => g.Count()) en LINQ ou Collectors.groupingBy(key, Collectors.counting()) en Java :

fn count_by[T](items []T, key fn (T) string) map[string]int {
mut counts := map[string]int{}
for item in items {
counts[key(item)]++
}
return counts
}
// Aucune contrainte : il suffit que T ait un champ `name` à chaque appel
fn names[T](items []T) []string {
return items.map(it.name)
}
println(count_by(projects, fn (p Project) string {
return p.language
}))
println(count_by[Project](projects, fn (p Project) string {
return p.sdk
}))
println(names(projects[..2]))
println(names(packages[..2]))
{'C#': 105, 'F#': 6}
{'Microsoft.NET.Sdk': 95, 'Microsoft.NET.Sdk.Web': 16}
['AllProjects.AppHost', 'AllProjects.ServiceDefaults']
['Aspire.Dashboard.Sdk.win-x64', 'Aspire.Hosting.AppHost']

ga a 105 projets C# et 6 projets F#, dont 16 projets web. V infère T à partir des arguments. Quand aucun argument n’est de type T, il faut le donner :

fn parse[T](field string) T {
return field.int()
}
fn main() {
println(parse('111'))
}
e05_infer.v:6:10: error: could not infer generic type `T` in call to `parse`
4 |
5 | fn main() {
6 | println(parse('111'))
| ~~~~~~~~~~~~
7 | }

names lit it.name sur un T que rien ne contraint. C# refuserait de compiler la fonction elle-même ; V vérifie à nouveau le corps pour chaque type avec lequel elle est appelée, comme un template C++. Project et Package ont tous deux un name. Un type qui n’en a pas échoue, et l’erreur pointe dans la fonction générique, pas sur l’appel :

struct Package {
id string
version string
}
fn names[T](items []T) []string {
return items.map(it.name)
}
fn main() {
println(names([Package{'Aspire.Hosting.AppHost', '9.5.1'}]))
}
e05_generic_field.v:7:22: error: type `Package` has no field named `name`.
2 possibilities: `id`, `version`.
5 |
6 | fn names[T](items []T) []string {
7 | return items.map(it.name)
| ~~~~
8 | }
9 |
e05_generic_field.v:7:15: error: cannot use `[]void` as type `[]string` in return argument
5 |
6 | fn names[T](items []T) []string {
7 | return items.map(it.name)
| ~~~~~~~~~~~~
8 | }
9 |

Index[T] est une map d’une clé vers un T, avec des méthodes. get renvoie une option, là où C# aurait TryGetValue et Java Optional<T> :

struct Index[T] {
mut:
by_key map[string]T
}
fn (mut ix Index[T]) add(key string, value T) {
ix.by_key[key] = value
}
fn (ix Index[T]) get(key string) ?T {
return ix.by_key[key] or { return none }
}
mut ix := Index[Project]{}
for p in projects {
ix.add(p.name, p)
}
println(ix.get('GaApi') or { Project{} }.path)
if p := ix.get('GaServer') {
println(p.path)
} else {
println('GaServer: not found')
}
println(typeof(ix).name)
Apps/ga-server/GaApi/GaApi.csproj
GaServer: not found
Index[Project]

Le nom est une mauvaise clé pour ga : deux projets s’appellent GaApi.Tests, dans Tests/Apps/GaApi.Tests et Tests/GaApi.Tests, donc le second add remplace le premier, et l’index contient 110 projets. Le chemin, lui, est unique.

largest compare avec >. C# le rejette dans la méthode générique elle-même (error CS0019: Operator '>' cannot be applied to operands of type 'T' and 'T') sauf si T a une contrainte. V l’accepte pour les nombres et les chaînes, et pour une struct qui définit < : V en génère >, <= et >= (surcharge d’opérateurs).

fn largest[T](items []T) ?T {
if items.len == 0 {
return none
}
mut best := items[0]
for x in items[1..] {
if x > best {
best = x
}
}
return best
}
struct Usage {
package string
projects int
}
fn (a Usage) < (b Usage) bool {
return a.projects < b.projects
}
println(largest(usages) or { Usage{} })
println(largest(by_package.values()) or { 0 })
println(largest(by_package.keys()) or { '' })
println(largest([]int{}) or { -1 })
Usage{
package: 'Spectre.Console'
projects: 20
}
20
xunit.runner.visualstudio
-1

Spectre.Console est le paquet que référencent le plus de projets de ga, 20 d’entre eux. Sans <, l’erreur est de nouveau dans la fonction, et sa formulation est curieuse :

fn main() {
println(largest([20, 12, 9]))
println(largest([Usage{'Spectre.Console', 20}, Usage{'xunit', 12}]))
}
e05_generic_operator.v:9:6: error: cannot use `>` as `<=` operator method is not defined
7 | mut best := items[0]
8 | for x in items[1..] {
9 | if x > best {
| ~~~~~~~~
10 | best = x
11 | }

Le message ne nomme pas Usage, et ne mentionne pas la ligne 18, l’appel qui le demande. Il ne nomme pas non plus <, l’opérateur à définir : > est généré à partir de <. Dans un programme avec plusieurs appels à largest, c’est à toi de trouver celui qui passe une struct.

Un test à la compilation au lieu d’une contrainte

Section intitulée « Un test à la compilation au lieu d’une contrainte »

Sans contraintes, une fonction générique peut tester son argument de type à la compilation avec $if T is … (types à la compilation), et T.name donne le nom du type :

interface Named {
name string
}
fn label[T](x T) string {
$if T is Named {
return '${T.name} named ${x.name}'
} $else {
return 'a ${T.name}'
}
}
println(label(projects[0]))
println(label(usages[0]))
println(label(42))
Project named AllProjects.AppHost
a Usage
a int

Seule la branche qui correspond à T est compilée, donc x.name n’est pas vérifié pour int. Contrairement à un where C#, le test ne rejette pas d’appel : il choisit le code pour chaque type.

  • Un type implémente une interface en ayant ses méthodes et ses champs ; la vérification a lieu là où une valeur est convertie, ou à la déclaration avec le implements facultatif.
  • Les interfaces peuvent exiger des champs. Les méthodes déclarées sur l’interface ne sont pas des implémentations par défaut.
  • Une méthode de la section mut: exige une valeur d’interface mutable ; un receveur mut ne peut pas implémenter une méthode hors de cette section.
  • Convertir une valeur struct en interface copie la struct, comme le boxing en C# ; convertir une référence la partage.
  • Les génériques n’ont pas de contraintes : le corps est vérifié pour chaque type utilisé, et les erreurs pointent dans la fonction générique plutôt que sur l’appel. $if T is choisit le code à la compilation.
  1. Écris fn group_by[T](items []T, key fn (T) string) map[string][]T, et affiche les noms des projets F# de ga.
Solution

examples/s05_group_by.v :

fn group_by[T](items []T, key fn (T) string) map[string][]T {
mut groups := map[string][]T{}
for item in items {
groups[key(item)] << item
}
return groups
}
groups := group_by(projects, fn (p Project) string {
return p.language
})
println(groups.keys())
println(groups['F#'].map(it.name))
['C#', 'F#']
['GaCli', 'GaMusicTheoryLsp', 'GA.Business.Config', 'GA.Business.Core.Generated', 'GA.Business.DSL', 'GA.Business.ProbabilisticGrammar']

groups[key(item)] << item fonctionne sur une clé absente : indexer une map pour la modifier crée le tableau vide, comme counts[key]++ crée le 0 dans count_by.

  1. Appelle largest sur les projets F# de l’exercice 1, de sorte qu’il renvoie le dernier par nom.
Solution

Project a besoin de < (s05_group_by.v, lignes 31-34) :

fn (a Project) < (b Project) bool {
return a.name < b.name
}
println(largest(groups['F#']) or { Project{} }.name)
GaMusicTheoryLsp

La comparaison de chaînes est ordinale, comme string.CompareOrdinal en C# et compareTo en Java : 'a' vient après 'A', donc GaMusicTheoryLsp vient après GA.Business.ProbabilisticGrammar.

  1. Dans l’exemple de copie, copied.total() vaut 1 et counter.n vaut 0. Qu’est-ce qui afficherait copied: counter.n = 1, et quel est l’équivalent en C# ?
Solution

Convertir une référence au lieu d’une valeur : mut counter := &Counter{}, puis Tally(counter), comme le fait l’exemple avec shared_counter. En C#, l’interface devrait désigner le même objet : fais de Counter une class au lieu d’une struct. Avec une struct, ITally copied = counter; met en boîte (boxing) une copie.