Découverte de Swift (2)

Après les bases du langage, prometteuses mais classiques, j'entre un peu plus dans le vif du sujet avec des notions nouvelles ou abordées d'une façon que je ne soupçonnais pas. Au programme : tuples, optionals, et fonctions.

Tuples

Un tuple, c'est tout simplement un ensemble de valeurs.

Par exemple, un code erreur et sa description réunis dans une même variable :

let erreur: (Int, String) = (404, "File Not Found")
let (code, description) = erreur
print("The error code is \(code)")
// Prints "The error code is 404"
print("The error description is \(description)")
// Prints "The error description is File Not Found"

Cela peut être particulièrement utile pour renvoyer plusieurs informations en retour d'une fonction, comme nous le verrons plus loin.

Optionals

Un optional est une une constante ou une variable qui accepte deux états possibles : – soit elle contient une valeur et peut-être utilisée – soit elle ne contient aucune valeur (nil)

Un exemple avec l'initialisation d'un entier à partir d'une chaîne de caractères

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?", or "optional Int"

let anotherPossibleNumber = "a random text"
let anotherConvertedNumber = Int(possibleNumber)
// anotherConvertedNumber is inferred to be of type "Int?", or "optional Int"

Selon que la chaine de caractères puisse être convertie ou non en nombre entier, la variable obtenue contiendra une valeur (dans le cas de convertedNumber) ou non (dans le cas de anotherConvertedNumber) : les deux variables sont alors considérées comme des optional Int, jusqu'à ce qu'on lui puisse s'assurer qu'ils contiennent bien une valeur.

Ceci est possible grâce à l'optional binding qui permet de combiner la vérification de l'existence d'une valeur et le cas échéant la récupération de cette valeur :

if let actualNumber = Int(possibleNumber) {
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(possibleNumber)\" could not be converted to an integer")
}
// Prints "The string "123" has an integer value of 123

if let actualNumber = Int(anotherPossibleNumber) {
    print("The string \"\(anotherPossibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(anotherPossibleNumber)\" could not be converted to an integer")
}
// Prints "The string "a random text" could not be converted to an integer

Fonctions

Les fonctions telles que Swift les proposent me semblent à la fois souples et riches en possibilités offertes.

Ce peut être une fonction toute simple, avec un seul paramètre et sans valeur en retour :

func sayHello(name: String) {
    print("Hello, \(name) !")
}

sayHello(name: "Zéro Janvier")
// Prints "Hello, Zéro Janvier !

La même fonction renvoyant le texte en retour au lieu de l'afficher directement :

func sayHello(name: String) -> String {
    return "Hello, \(name) !"
}

print(sayHello(name: "Zéro Janvier"))
// Prints "Hello, Zéro Janvier !

Swift propose pour les fonctions différentes possibilités selon les cas d'usage : paramètres nommés ou non, valeurs par défaut pour certains paramètres, etc.

Là où ça devient encore plus intéressant, c'est quand on commence à combiner fonctions, tuples et optionals.

Par exemple, imaginons une fonction qui reçoit un tableau d'entiers et renvoie la valeur maximale, la valeur minimale et la somme des entiers reçus en entrée. Si le tableau d'entiers reçu est vide, il est impossible de calculer le minimum et le maximum, les valeurs renvoyées par la fonction seront donc indéterminées :

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int)? {
    
    if scores.isEmpty { return nil}
    
    var min = scores[0]
    var max = scores[0]

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
    }

    return (min, max)
}

let myScores = [5, 3, 100, 3, 9]
if let statistics = calculateStatistics(scores: myScores) {
    print(statistics.min)   // Prints 3
    print(statistics.max)   // Prints 100
}
else
{
    print("Unable to calculate statistics")
}

let myAnotherScores: [Int] = []
if let statistics = calculateStatistics(scores: myAnotherScores) {
    print(statistics.min)
    print(statistics.max)
}
else
{
    print("Unable to calculate statistics") // Prints "Unable to calculate statistics"
}

Fonctions et Types : fonctions génériques

Mais le gros morceau, ce qui m'a véritablement ravi c'est la façon dont Swift conçoit les fonctions. Dans Swift, une fonction possède un type, de la même façon qu'une constante ou qu'une variable sont typées. Le type d'une fonction est défini par les paramètres qu'elle reçoit en entrée et ce qu'elle renvoie en retour.

Ainsi, je peux définir 2 fonctions simples qui se contentent respectivement d'additionner et de multiplier deux nombres entiers et de renvoyer le résultat :

func addInteger(firstInteger: Int, secondInteger: Int) -> Int {
    return firstInteger + secondInteger
}

func multiplyInteger(firstInteger: Int, secondInteger: Int) -> Int {
    return firstInteger * secondInteger
}

Ensuite, je peux déclarer une fonction générique ayant le même type que mes 2 fonctions, c'est-à-dire une fonction qui reçoit 2 paramètres de type Int et qui renvoie un Int :

var myMathFunction: (Int, Int) -> Int

Ou mieux, utiliser ce type de fonction comme paramètre pour passer l'une ou l'autre de mes fonctions à une autre fonction :

func printMathResult(mathFunction: (Int, Int) -> Int, firstInteger: Int, secondInteger: Int) {
    let myResult = mathFunction(firstInteger, secondInteger)
    print(myResult)
}

let myFirstInteger = 3
let mySecondInteger = 7

printMathResult(mathFunction: addInteger, firstInteger: myFirstInteger, secondInteger: mySecondInteger)
printMathResult(mathFunction: multiplyInteger, firstInteger: myFirstInteger, secondInteger: mySecondInteger)

J'ai ainsi créé une fonction qui permet d'afficher le résultat d'une opération arithmétique entre deux entiers, quelle que soit l'opération en question, puisque l'opération à réaliser est directement passée en paramètre pour être appelée avant d'afficher le résultat.

C'est sans doute très courant dans la plupart des langages modernes, mais c'est tellement loin de ce que je vois au quotidien dans mon travail que je ne peux pas m'empêcher d'être enthousiaste, voire émerveillé.

Dans le prochain billet, je parlerai de closures. Pour être franc, j'avais initialement prévu d'en parler dans ce billet, mais j'ai été tellement bavard sur les fonctions et la richesse qu'elles offrent dans Swift que j'ai préféré m'arrêter là pour aujourd'hui. Je consacrerai un billet à part entière aux closures, un autre sujet passionnant et riche en opportunités.

#swift

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