Découverte de Swift (3)

Au programme aujourd'hui : les fameuses closures, une découverte pour moi.

Closures

Les closures, ce sont des blocs de code qui peuvent être transmis et réutilisés ailleurs dans le code. Dis comme ça, ce n'est pas forcément très clair. Moi-même, quand j'ai lu cette définition dans le bouquin, j'ai hoché la tête en me disant que ce serait peut-être plus clair avec un exemple.

Un 1er exemple (simple)

Imaginons que je dispose d'un tableau contenant le prénom de plusieurs de mes amis et que je souhaite les trier par ordre alphabétique décroissant (de Z à A plutôt que de A à Z).

Swift propose pour les tableaux une fonction sorted(by:) qui accepte en paramètre by: une closure, prenant elle-même 2 paramètres du même type que les éléments du tableau et renvoyant un booléen indiquant si le premier élément reçu en paramètre doit apparaître avant (true) ou après (false) le second élément.

Si on applique cela à notre tableau de chaînes de caractères (String) contenant des prénoms, on peut commencer ainsi :

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

func ReverseAlphabeticSort(_ s1: String, _ s2 : String) -> Bool {
    return s1 > s2
}

var reversedNames = names.sorted(by: ReverseAlphabeticSort)
print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]

Pour les chaînes de caractères, la comparaison standard se base sur l'ordre alphabétique. Nous créons donc une fonction qui prend 2 chaînes de caractères et renvoie true si la première chaîne est “supérieure” à la seconde, false dans le cas contraire. Ainsi, notre fonction inverse l'ordre alphabétique du tri standard.

Il suffit ensuite de passer notre fonction comme paramètre by: à la fonction sorted du tableau, et le tour est joué.

C'est pratique, mais cela reste un peu trop verbieux. Une closure peut en réalité être abrégée. Faisons cela en plusieurs étapes :

Tout d'abord, passons nous d'une fonction déclarée au préalable et codons directement la condition de tri en tant que paramètre de la fonction sorted:

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

var reversedNames = names.sorted { (s1: String, s2: String) -> Bool in return s1 > s2 }

print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]


Nous sommes face à la syntaxe standard d'une closure :

{ (parameters) -> returnType in
	statements

Dans notre cas, nous avons 2 paramètres de type String et un Boolen retour.

Allons plus loin encore dans la simplification de notre code.

Le type des paramètres et de la valeur en retour n'ont pas forcément besoin d'être explicités puisqu'ils peuvent être déduits implicitement du contexte. Notre closure est fournie à la fonction sorted(by:)dont nous savons qu'elle attend 2 paramètres de type Stringet un retour de type Bool. Nous pouvons donc simplifier notre code en omettant le typage des paramètres et du retour :

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

var reversedNames = names.sorted { (s1, s2) in return s1 > s2 }

print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]

Encore plus simple, le returnn'a pas forcément besoin d'être explicite dans le cas où le code de la closure ne contient qu'une seule instruction :

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

var reversedNames = names.sorted { s1, s2 in s1 > s2 }

print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]

Allons encore plus loin : Swift permet de ne pas nommer explicitivement les paramètres et d'utiliser les noms génériques $0 (pour le premider paramètre), $1 (pour le deuxième paramètre), et ainsi de suite s'il y a plus de 2 paramètres. Dans notre cas, cela réduit le code ainsi :

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

var reversedNames = names.sorted { $0 > $1 }

print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]

Enfin, dernière simplification : l'opérateur >étant défini pour le type standard String, il est possible de l'utiliser directement comme paramètre de la fonction sorted:by. En effet, cet opérateur a la particularité (comme tous les opérateurs de comparaison) d'être du type attendu : une fonction qui prend 2 paramètres de type Stringet renvoie un Bool. En simplifiant à l'extrême, on obtient ainsi le code suivant :

let names = ["Christophe", "Alexandre", "Etienne", "Baptiste", "Daniel"]

var reversedNames = names.sorted(by: >)

print(reversedNames)
// Prints ["Etienne", "Daniel", "Christophe", "Baptiste", "Alexandre"]

Un 2nd exemple (un peu plus complexe)

Nous allons maintenant aborder une autre fonctionnalité des closures : la capture de valeurs, c'est-à-dire le fait de pouvoir accéder à des variables ou constantes qui ne sont normalement pas ou plus dans le scope de la closure lors de son exécution.

Prenons un exemple pour illustrer cela :

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    
    return incrementer
}

Nous avons créé une fonction makeIncrementerqui reçoit en paramètre un nombre entier et qui renvoie une fonction sans paramètre mais qui renvoie un entier.

Cette fonction, c'est un incrémenteur, dont le but est de retourner un compteur (runningTotal) qui s'incrémente à chaque appel du même pas d'incrément (amount) défini à sa création.

Cela nous permet de créer par exemple 2 incrémenteurs distincts, l'un avec un incrément de 10, l'autre avec un incrément de 5. Chaque incrémenteur fonctionne ensuite de façon indépendante, avec son propre pas d'incrément et son même compteur :

met incrementBy10 = makeIncrementer(forIncrement: 10)
let incrementBy5 = makeIncrementer(forIncrement: 5)

print(incrementBy10()) // Prints 10
print(incrementBy10()) // Prints 20
print(incrementBy10()) // Prints 30

print(incrementBy5())  // Prints 5
print(incrementBy5())  // Prints 10
print(incrementBy5())  // Prints 15

print(incrementBy10()) // Prints 40

Dans la suite du chapitre sur les closures, j'ai vu quelques subtilités que je n'ai pas forcément totalement comprises, et qui vont clairement nécessiter une relecture posée et surtout une mise en pratique pour mieux comprendre ce dont il s'agit et ce que cela permet.

Et la suite ?

Dans le prochain billet, nous aborderons les énumérations, qui sujet qui semble classique et simple mais tout de même riche en fonctionnalités.

#swift

Zéro Tech, le blog Tech de Zéro Janvier@zerojanvier@mamot.fr