Fonctions d'ordre supérieur avec les collections

1. Introduction

Dans l'atelier de programmation Utiliser les types de fonction et les expressions lambda en Kotlin, vous avez découvert les fonctions d'ordre supérieur, c'est-à-dire celles qui utilisent d'autres fonctions en tant que paramètres et/ou renvoient une fonction, telle que repeat(). Les fonctions d'ordre supérieur sont particulièrement pertinentes pour les collections, car elles vous permettent d'effectuer des tâches courantes, comme le tri ou le filtrage, en utilisant moins de code. Maintenant que vous disposez d'une base solide pour travailler avec des collections, il est temps de s'intéresser de plus près aux fonctions d'ordre supérieur.

Dans cet atelier de programmation, vous allez découvrir plusieurs fonctions utilisables sur les types de collection, notamment forEach(), map(), filter(), groupBy(), fold() et sortedBy(). Tout au long du processus, vous vous entraînerez également à utiliser les expressions lambda.

Conditions préalables

  • Bonne connaissance des types de fonctions et des expressions lambda.
  • Bonne connaissance de la syntaxe des lambdas de fin, comme la fonction repeat().
  • Connaissance des différents types de collections en Kotlin, tels que List.

Points abordés

  • Comment intégrer des expressions lambda dans des chaînes.
  • Comment utiliser différentes fonctions d'ordre supérieur avec la collection List, notamment forEach(), map(), filter(), groupBy(), fold() et sortedBy().

Ce dont vous avez besoin

  • Un navigateur Web ayant accès à Kotlin Playground

2. forEach() et modèles de chaîne avec lambdas

Code de démarrage

Dans les exemples suivants, vous allez utiliser une collection List représentant le menu des cookies d'une boulangerie (de quoi vous donner l'eau à la bouche !), et utiliser des fonctions d'ordre supérieur pour le mettre en forme de différentes manières.

Commencez par configurer le code de démarrage.

  1. Accédez à Kotlin Playground.
  2. Au-dessus de la fonction main(), ajoutez la classe Cookie. Chaque instance de Cookie représente un élément du menu, avec un name (nom), un price (prix) et d'autres informations sur le cookie.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. Sous la classe Cookie, en dehors de main(), créez une liste de cookies comme indiqué. Le type est supposé être List<Cookie>.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

val cookies = listOf(
    Cookie(
        name = "Chocolate Chip",
        softBaked = false,
        hasFilling = false,
        price = 1.69
    ),
    Cookie(
        name = "Banana Walnut",
        softBaked = true,
        hasFilling = false,
        price = 1.49
    ),
    Cookie(
        name = "Vanilla Creme",
        softBaked = false,
        hasFilling = true,
        price = 1.59
    ),
    Cookie(
        name = "Chocolate Peanut Butter",
        softBaked = false,
        hasFilling = true,
        price = 1.49
    ),
    Cookie(
        name = "Snickerdoodle",
        softBaked = true,
        hasFilling = false,
        price = 1.39
    ),
    Cookie(
        name = "Blueberry Tart",
        softBaked = true,
        hasFilling = true,
        price = 1.79
    ),
    Cookie(
        name = "Sugar and Sprinkles",
        softBaked = false,
        hasFilling = false,
        price = 1.39
    )
)

fun main() {

}

Faire une boucle sur une liste avec forEach()

La première fonction d'ordre supérieur que vous connaissez est forEach(). La fonction forEach() exécute une seule fois la fonction transmise en tant que paramètre pour chaque élément de la collection. Le fonctionnement est semblable à celui de la fonction repeat(), ou à celui d'une boucle for. Le lambda est exécuté pour le premier élément, puis pour le deuxième, et ainsi de suite pour chaque élément de la collection. Le prototype de la méthode est le suivant :

forEach(action: (T) -> Unit)

forEach() n'accepte qu'un seul paramètre d'action, à savoir une fonction de type (T) -> Unit.

T correspond au type de données que contient la collection. Étant donné que le lambda n'utilise qu'un seul paramètre, vous pouvez omettre son nom et y faire référence avec it.

Utilisez la fonction forEach() pour afficher les éléments de la liste cookies.

  1. Dans main(), appelez forEach() sur la liste cookies en utilisant la syntaxe d'un lambda de fin. Étant donné que le lambda de fin est le seul argument, vous pouvez omettre les parenthèses lorsque vous appelez la fonction.
fun main() {
    cookies.forEach {

    }
}
  1. Dans le corps du lambda, ajoutez une instruction println() qui affiche it.
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. Exécutez votre code et observez le résultat. Le nom du type (Cookie) et un identifiant unique de l'objet s'affichent, mais pas le contenu de l'objet.
Menu item: Cookie@5a10411
Menu item: Cookie@68de145
Menu item: Cookie@27fa135a
Menu item: Cookie@46f7f36a
Menu item: Cookie@421faab1
Menu item: Cookie@2b71fc7e
Menu item: Cookie@5ce65a89

Intégrer des expressions dans des chaînes

Lorsque vous avez découvert les modèles de chaîne, vous avez vu comment utiliser le caractère dollar ($) avec un nom de variable pour l'insérer dans une chaîne. Toutefois, cette technique ne fonctionne pas comme prévu lorsqu'elle est associée à l'opérateur point (.) pour accéder aux propriétés.

  1. Dans l'appel à forEach(), modifiez le corps du lambda pour insérer $it.name dans la chaîne.
cookies.forEach {
    println("Menu item: $it.name")
}
  1. Exécutez votre code. Notez que nom de la classe, Cookie, est inséré, ainsi qu'un identifiant unique pour l'objet suivi de .name. Le code n'accède pas à la valeur de la propriété name.
Menu item: Cookie@5a10411.name
Menu item: Cookie@68de145.name
Menu item: Cookie@27fa135a.name
Menu item: Cookie@46f7f36a.name
Menu item: Cookie@421faab1.name
Menu item: Cookie@2b71fc7e.name
Menu item: Cookie@5ce65a89.name

Pour accéder aux propriétés et les intégrer dans une chaîne, vous avez besoin d'une expression. Vous pouvez insérer une expression dans un modèle de chaîne en l'entourant d'accolades.

2c008744cee548cc.png

L'expression lambda est placée entre accolades. Vous pouvez accéder aux propriétés, effectuer des opérations mathématiques, appeler des fonctions, etc., et la valeur renvoyée par le lambda est insérée dans la chaîne.

Modifions le code pour que le nom soit inséré dans la chaîne.

  1. Entourez it.name d'accolades pour en faire une expression lambda.
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. Exécutez votre code. La sortie contient le name (nom) de chaque Cookie.
Menu item: Chocolate Chip
Menu item: Banana Walnut
Menu item: Vanilla Creme
Menu item: Chocolate Peanut Butter
Menu item: Snickerdoodle
Menu item: Blueberry Tart
Menu item: Sugar and Sprinkles

3. map()

La fonction map() vous permet de transformer une collection en une nouvelle collection avec le même nombre d'éléments. Par exemple, map() peut transformer List<Cookie> en List<String> ne contenant que le name (nom) du cookie, à condition que vous indiquiez à la fonction map() comment créer une String (chaîne) à partir de chaque élément Cookie.

e0605b7b09f91717.png

Imaginons que vous écrivez une application qui affiche un menu interactif pour une boulangerie. Lorsque l'utilisateur accède à l'écran qui affiche le menu des cookies, il est probable qu'il souhaite voir les données présentées de manière logique, comme le nom suivi du prix. Vous pouvez créer une liste de chaînes, formatées avec les données appropriées (nom et prix) à l'aide de la fonction map().

  1. Supprimez tout le code précédent de main(). Créez une variable appelée fullMenu et définissez-la sur le résultat de l'appel de map() sur la liste cookies.
val fullMenu = cookies.map {

}
  1. Dans le corps du lambda, ajoutez une chaîne formatée pour inclure le name (nom) et le price (prix) de it (le cookie).
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. Affichez le contenu de fullMenu. Pour ce faire, vous pouvez utiliser forEach(). La collection fullMenu renvoyée par map() est de type List<String> au lieu de List<Cookie>. Chaque Cookie dans cookies correspond à une String dans fullMenu.
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. Exécutez votre code. La sortie correspond au contenu de la liste fullMenu.
Full menu:
Chocolate Chip - $1.69
Banana Walnut - $1.49
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Sugar and Sprinkles - $1.39

4. filter()

La fonction filter() vous permet de créer un sous-ensemble dans une collection. Par exemple, si vous avez une liste de nombres, vous pouvez utiliser filter() pour créer une liste contenant uniquement les nombres pairs.

d4fd6be7bef37ab3.png

Alors que le résultat de la fonction map() génère toujours une collection de même taille, filter() peut également générer une collection plus petite que celle d'origine. Contrairement à map(), la collection finale possède également le même type de données. Par conséquent, filtrer une List<Cookie> crée une autre List<Cookie>.

Comme map() et forEach(), filter() utilise une seule expression lambda comme paramètre. Le lambda possède un seul paramètre représentant chaque élément de la collection et renvoie une valeur Boolean.

Pour chaque élément de la collection :

  • Si le résultat de l'expression lambda est true, l'élément est inclus dans la nouvelle collection.
  • Si le résultat est false, l'élément n'est pas inclus dans la nouvelle collection.

Cette technique est utile si vous souhaitez obtenir un sous-ensemble de données dans votre application. Par exemple, imaginons que la boulangerie souhaite mettre en avant ses cookies moelleux dans une section distincte du menu. Vous pouvez commencer par utiliser filter() sur la liste cookies, puis afficher les éléments.

  1. Dans main(), créez une variable appelée softBakedMenu et définissez-la sur le résultat de l'appel de filter() sur la liste cookies.
val softBakedMenu = cookies.filter {
}
  1. Dans le corps du lambda, ajoutez une expression booléenne pour vérifier que la propriété softBaked du cookie est égale à true. Comme softBaked est un Boolean lui-même, le corps du lambda ne doit contenir que it.softBaked.
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. Affichez le contenu de softBakedMenu à l'aide de forEach().
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Exécutez votre code. Le menu s'affiche comme auparavant, mais il ne comprend que les cookies moelleux.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. groupBy()

La fonction groupBy() permet de transformer une liste en mappage. Chaque valeur renvoyée unique de la fonction devient une clé du mappage obtenu. Les valeurs de chaque clé correspondent à tous les éléments de la collection qui ont produit cette valeur renvoyée unique.

54e190b34d9921c0.png

Le type de données des clés est identique au type renvoyé par la fonction transmise dans groupBy(). Le type de données de ces valeurs correspond à la liste des éléments d'origine.

Ce concept peut être difficile à comprendre, donc prenons un exemple simple. À partir de la même liste qu'avant, groupez les nombres par valeurs paires et impaires.

Vous pouvez vérifier si un nombre est pair ou impair en le divisant par 2, puis en observant si le reste après division est 0 ou 1. Si le reste est 0, le nombre est pair. Dans le cas contraire, si le reste est 1, le nombre est impair.

Pour ce faire, vous pouvez utiliser l'opérateur modulo (%), qui divise la valeur de gauche (le dividende) par la valeur de droite (le diviseur).

4c3333da9e5ee352.png

Au lieu de renvoyer le résultat de la division, comme c'est le cas avec l'opérateur de division (/), l'opérateur modulo renvoie le reste après division, ce qui permet par exemple de vérifier si un nombre est pair ou impair.

4219eacdaca33f1d.png

La fonction groupBy() est appelée avec l'expression lambda suivante : { it % 2 }.

Le mappage obtenu comprend deux clés : 0 et 1. Chaque clé a une valeur de type List<Int>. La liste de la clé 0 contient tous les nombres pairs et la liste de la clé 1 contient tous les nombres impairs.

Un cas d'utilisation concret peut être une application de photos qui regroupe les clichés en fonction du sujet ou du lieu de prise de vue. Pour notre menu de boulangerie, nous allons regrouper les éléments pour différencier les cookies moelleux des autres.

Utilisez groupBy() pour réorganiser le menu en fonction de la propriété softBaked.

  1. Supprimez l'appel à filter() du code de l'étape précédente.

Code à supprimer

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Appelez groupBy() sur la liste cookies et stockez le résultat dans une variable appelée groupedMenu.
val groupedMenu = cookies.groupBy {}
  1. Transmettez une expression lambda qui renvoie it.softBaked. Le type renvoyé est Map<Boolean, List<Cookie>>.
val groupedMenu = cookies.groupBy { it.softBaked }
  1. Créez une variable softBakedMenu contenant la valeur de groupedMenu[true] et une variable crunchyMenu contenant la valeur de groupedMenu[false]. Étant donné que le résultat de la mise en indice d'une Map peut être nul, vous pouvez utiliser l'opérateur Elvis (?:) pour renvoyer une liste vide.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. Ajoutez du code pour afficher le menu des cookies moelleux suivi de celui des cookies croustillants.
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Exécutez votre code. En utilisant la fonction groupBy(), la liste est divisée en deux, en fonction de la valeur de l'une des propriétés.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Crunchy cookies:
Chocolate Chip - $1.69
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Sugar and Sprinkles - $1.39

6. fold()

La fonction fold() permet de générer une valeur unique à partir d'une collection. Elle est principalement utilisée pour calculer un prix total ou pour additionner tous les éléments d'une liste afin de calculer une moyenne.

a9e11a1aad05cb2f.png

La fonction fold() nécessite deux paramètres :

  • La valeur initiale. Le type de données est fixé lors de l'appel de la fonction (par exemple, une valeur initiale de 0 fixe le type de valeur sur Int).
  • Une expression lambda qui renvoie une valeur du même type que la valeur initiale.

L'expression lambda comporte également deux paramètres :

  • Le premier est l'accumulateur. Il a le même type de données que la valeur initiale. Voyez cela comme un total cumulé. Chaque fois que le lambda est appelé, l'accumulateur est égal à la valeur renvoyée lors du dernier appel de ce même lambda.
  • Le second est du même type que chaque élément de la collection.

Comme pour les autres fonctions que vous avez vues, l'expression lambda est appelée pour chaque élément d'une collection. Vous pouvez donc utiliser fold() pour additionner tous les éléments de façon concise.

Utilisons fold() pour calculer le prix total de tous les cookies.

  1. Dans main(), créez une variable appelée totalPrice et définissez-la sur le résultat de l'appel de fold() sur la liste cookies. Transmettez 0.0 comme valeur initiale. Son type est donc fixé sur Double.
val totalPrice = cookies.fold(0.0) {
}
  1. Vous devez spécifier les deux paramètres pour l'expression lambda. Utilisez total pour l'accumulateur et cookie pour l'élément de collection. Utilisez une flèche (->) après la liste des paramètres.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. Dans le corps du lambda, calculez la somme de total et de cookie.price. Cette somme devient la valeur renvoyée et est transmise à total la prochaine fois que le lambda est appelé.
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. Affichez la valeur de totalPrice, sous la forme d'une chaîne pour une meilleure lisibilité.
println("Total price: $${totalPrice}")
  1. Exécutez votre code. Le résultat doit être égal à la somme des prix de la liste cookies.
...
Total price: $10.83

7. sortedBy()

Lorsque vous avez découvert les collections, vous avez appris que la fonction sort() pouvait être utilisée pour trier les éléments. Cependant, cette fonction ne fonctionnera pas sur une collection d'objets Cookie. La classe Cookie comporte plusieurs propriétés, et Kotlin ne peut pas savoir sur lesquelles effectuer le tri (name, price, etc.).

Pour ce type de situations, les collections Kotlin proposent la fonction sortedBy(). sortedBy() vous permet de spécifier un lambda qui renvoie la propriété sur laquelle vous souhaitez effectuer le tri. Par exemple, si vous souhaitez effectuer un tri en fonction de la propriété price, le lambda renvoie it.price. Tant que le type de données de la valeur possède un ordre de tri naturel (les chaînes sont triées par ordre alphabétique et les valeurs numériques triées par ordre croissant), les données sont triées de la même façon qu'une collection de ce type.

5fce4a067d372880.png

Essayons d'utiliser sortedBy() pour trier la liste des cookies par ordre alphabétique.

  1. Dans main(), après le code existant, ajoutez une variable appelée alphabeticalMenu et attribuez-lui la valeur renvoyée par l'appel de la fonction sortedBy() sur la liste cookies.
val alphabeticalMenu = cookies.sortedBy {
}
  1. Dans l'expression lambda, renvoyez it.name. La liste obtenue sera de type List<Cookie>, mais triée en fonction de name.
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. Affichez le nom des cookies dans alphabeticalMenu. Vous pouvez utiliser forEach() pour afficher chaque nom sur une nouvelle ligne.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. Exécutez votre code. Les noms des cookies sont affichés par ordre alphabétique.
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. Conclusion

Félicitations ! Vous venez de voir plusieurs exemples d'utilisation de fonctions d'ordre supérieur sur des collections. Les opérations les plus courantes, comme le tri et le filtrage, peuvent être effectuées avec une seule ligne de code, ce qui rend vos programmes plus clairs et concis.

Résumé

  • Vous pouvez faire une boucle qui s'applique successivement à chaque élément d'une collection à l'aide de forEach().
  • Les expressions peuvent être insérées dans des chaînes.
  • map() permet de mettre en forme les éléments d'une collection, souvent en les organisant dans une collection utilisant un autre type de données.
  • filter() peut générer un sous-ensemble d'une collection.
  • groupBy() divise une collection en fonction de la valeur renvoyée par une fonction.
  • fold() transforme une collection en une valeur unique.
  • sortedBy() permet de trier une collection selon une propriété spécifiée.

9. En savoir plus