Les extensions avec Swift 2

Quand on manipule des données on a historiquement le réflexe de créer des fonctions globales, ainsi que d'utiliser des boucles, pour manipuler des collections telles des array.

Avec Swift 2, grâce aux extensions, nous allons voir qu'il est facile de se créér des collections dont le comportement est personnalisé, et que l'on n'a pas besoin de boucles ni de fonctions globales.

Le code est plus simple d'usage, expressif, et bien évidemment très puissant, c'est l'intérêt.

Extend all the things!

Extensions

Qu'est-ce donc ? Pour citer la documentation :

Les extensions ajoutent de nouvelles fonctionnalités à des objets existants tels des classes, structures, enumerations ou protocoles.

Attention, ce n'est pas la même chose que l'héritage en orienté objet, même si ça y ressemble : ici on étend les capacités d'un type, on n'hérite pas des capacités d'un autre objet.

Mais nous allons voir ça étape par étape.

Collections

Pour ce tutorial disons que nous cherchons à représenter divers objets sous forme de collection, par exemple en array : si nous avons un mot, nous voulons un array de lettres. Si nous avons un nombre, nous voulons un array de chiffres. Si nous avons une liste de films, nous voulons un array de leurs années de sortie. Etc.

Source :

Hello

2015

Resultat souhaité :

[“H”, “e”, “l”, “l”, “o”]

[2, 0, 1, 5]

Fonction globale

Le coup classique est de créer une fonction globale qui effectue la tâche souhaitée.

Exemple pour transformer un mot en array de lettres :

func wordToLetters(word: String) -> [String] {
    var arrayOfLetters:[String] = []
    for letter in "Hello".characters {
        arrayOfLetters.append(String(letter))
    }
    return arrayOfLetters
}

Bon, ça marche, mais il y a plusieurs problèmes.

Tout d'abord ça nous oblige à utiliser une fonction globale, et donc à se souvenir de son existence et de son nom. Savoir qu'il existe dans notre codebase une fonction “wordToLetters” qui exécute cette tâche n'est pas intuitif :

let arrayOfLetters = wordToLetters("Hello")

Ensuite, dans la fonction elle-même, nous avons créé un nouvel array vide, puis nous faisons une boucle dans les caractères et ajoutons chacun dans notre array avant de renvoyer celui-ci.

C'est peu efficace, il y a mieux à faire !

Etendons-donc le type String

En utilisant une extension au type String lui-même nous lui ajoutons une propriété calculée (computed property) que nous allons par exemple nommer “letters”:

extension String {
    var letters:[String] {
        return self.characters.map { String($0) }
    }
}

Ici nous prenons le type String et lui ajoutons une nouvelle propriété : nous l'avons étendu.

Nous avons aussi remplacé la boucle par map qui itère sur la collection de caractères, ici représentée par self (puisque nous sommes dans une extension de String, self est la String en cours d'usage), et pour chaque caractère en cours d'itération (représenté par $0) crée une nouvelle String (donc une une lettre) puis retourne la collection.

Usage :

let result = "Hello".letters

Résultat :

[“H”, “e”, “l”, “l”, “o”]

Bien plus expressif qu'une fonction globale, n'est-ce pas? Et de plus on bénéficie de l'auto-suggestion dans Xcode.

Notez qu'avec Swift il y a le type String qui peut comporter un ou plusieurs objets de type Character, et il y a, donc, le type Character. Ces deux types sont différents. On peut transformer un Character en String de plusieurs façons, notamment avec String(char).

Même chose avec Int

Nous allons maintenant faire la même chose avec des nombres entiers, et pour cela nous allons étendre le type Int qui représente des Integers.

Mais comment séparer chaque digit d'un même nombre ? Passons d'abord le nombre en lettres et utilisons notre précédente extension, pardi !

extension Int {
    var digits:[Int] {
        return String(self).letters.flatMap { Int($0) }
    }
}

Avec String(self) nous transformons le nombre en chaîne de caractères, puis nous appliquons à ce mot notre précédente extension letters, puis nous mappons de nouveau chaque lettre en un chiffre avec Int().

Usage :

let arrayOfDigits = 789.digits

Résultat :

[7, 8, 9]

Donc 789 est devenu “789” puis [“7”, “8”, “9”] puis enfin [7, 8, 9].

C'est beau !

Encore plus

Nous avons donc une extension digits qui s'applique sur un Integer. Pourrait-on faire la même chose sur une String ? C'est-à-dire, extraire les integers d'une string ?

Tout simplement en se basant sur nos travaux précédents :

extension String {
    var digits:[Int] {
        return self.letters.flatMap { Int($0) }
    }
}

On itère sur les caractères et on transforme chacun en Int avant de renvoyer le tout dans un array.

Usage :

print("X4Z3-82b".digits)

Résultat :

[4, 3, 8, 2]

Et… comment faire l'inverse ? Retirer les chiffres d'une chaîne de caractères :

extension String {
    var withoutDigits:[String] {
        return self.letters.filter { Int($0) == nil }
    }  
}

L'astuce c'est qu'on filtre les caractères qui ne sont pas transformables en Int, et il ne reste que les lettres… ou plutôt, les caractères qui ne sont pas des chiffres :

print("X4Z3-82b".withoutDigits)

[“X”, “Z”, “-”, “b”]

Appliquer des transformations

On peut se baser sur le même système pour transformer des éléments en collections.

Par exemple, on veut créer un acronyme. Pour ça, on veut ne garder que la première lettre de chaque mot si elle est en majuscule (et ignorer le reste). On recolle ensuite l'array pour créer l'acronyme :

extension String {
    var acronym:String {
        return self.withoutDigits.filter { $0 != " " }.filter { $0 == $0.uppercaseString }.joinWithSeparator("")
    }
}

Usage :

let title = "Industrial Light and Magic 1992"
print(title.acronym)

Résultat :

“ILM”

Extensions de protocoles

Jusqu'ici nous avons étendu des types concrets : String et Int.

Mais on peut aussi étendre des protocoles. Ainsi tous les objets qui se conforment à ce protocole bénéficieront de notre extension.

Par exemple, pour que notre extension soit compatible avec array nous allons étendre CollectionType qui est le protocole associé à ces collections.

On va en profiter pour contraindre l'extension : par exemple faire x sur array de Strings mais faire y sur array de Ints.

Reprenons notre premier exercice : transformer un mot en lettres. Comment l'adapter pour travailler sur un array de mots ? On ne va quand même pas faire des boucles comme un animal ! ;)

Tout simplement :

extension CollectionType where Generator.Element == String {
    var arraysOfLetters:[[String]] {
        return self.flatMap { $0.letters }
    }
}

Woah. Alors, dans l'ordre :

Usage :

let words = ["Hello", "world!"]
print(words.arraysOfLetters)

Résultat :

[[“H”, “e”, “l”, “l”, “o”], [“w”, “o”, “r”, “l”, “d”, “!”]]

Et comment faire si on veut que toutes les lettres soient en fait dans un seul array ? On applatit le résultat précédent avec flatten :

extension CollectionType where Generator.Element == String {
    var letters:[String] {
        return Array(self.arraysOfLetters.flatten())
    }
}

Usage :

print(words.letters)

Résultat :

[“H”, “e”, “l”, “l”, “o”, “w”, “o”, “r”, “l”, “d”, “!”]

Unique

Au passage, une astuce pour transformer un array d'éléments dont certains se répètent en un array d'éléments uniques : utiliser un Set (qui est une collection d'objets uniques, automatique) et transformer le résultat en array dans une extension !

Exemple pour des nombres :

extension CollectionType where Generator.Element == Int {
    var uniques:[Int] {
        return Array(Set(self))
    }
}

Usage :

let years = [1982, 1982, 1984, 1986, 1986]
print(years.uniques)

Résultat :

[1982, 1984, 1986]

Et si je vous montre ça, ce n'est pas par hasard, comme on va le voir maintenant. :)

Extensions pour objets

Nous avons vu comment étendre des types (String, Int) et des protocoles (CollectionType). Voyons maintenant comment étendre des objets (class, struct, etc). On va pour cela continuer à utiliser des extensions, mais aussi directement un protocole.

Disons que nous voulons un objet “Movie” qui contienne un titre et une année de sortie.

Pour notre tuto, on va faire ça avec une struct toute simple :

struct Movie {
    var title:String
    var year:Int
}

Usage : on créée un film grâce à l'initializer automatique de la struct.

let ghost = Movie(title: "Ghostbusters", year: 1984)

Comme d'habitude on peut ensuite accéder aux propriétés de l'objet :

ghost.title
ghost.year

affichent respectivement

“Ghostbusters”

et

1984

Se conformer au protocole

En plus du titre et de l'année, nous voudrions que l'objet soit capable d'afficher une petite description. Pour un film ce pourrait être

“Titre (année)”

exemple :

“Ghostbusters (1984)”

Au lieu de faire ça “dans notre coin”, nous allons adopter le protocole CustomStringConvertible qui décrit simplement l'existence d'une propriété description dans l'objet.

Cette propriété description peut contenir et retourner ce que l'on veut, le protocole s'en fiche : il garantit seulement son existence.

On se conforme au protocole et on ajoute notre version de description :

struct Movie: CustomStringConvertible {
    var title:String
    var year:Int
}

extension Movie {
    var description:String {
        return "\(title) (\(year))"
    }
}

Ce qui nous donne :

movie.description

Ghostbusters (1984)

Note : nous aurions pu étendre Movie de la même façon sans se conformer au protocole, mais en s'y conformant on indique à Swift que cet objet est conforme et donc désormais compatible avec toutes les autres opérations compatibles avec le protocole.

Cela signifie que de par sa conformance au protocle CustomStringConvertible, notre objet bénéficie d'autres comportements typiques de ce protocole. Dans notre cas, ça a un intéressant effet de bord sur print.

Faites le test dans un nouveau Playground. Collez ce code 

struct MovieA {

    var title:String
    var year:Int

    var description:String {
        return "\(title) (\(year))"
    }

}

let bttfA = MovieA(title: "Back to the Future", year: 1985)
print(bttfA)


struct MovieB: CustomStringConvertible {

    var title:String
    var year:Int

    var description:String {
        return "\(title) (\(year))"
    }

}

let bttfB = MovieB(title: "Back to the Future", year: 1985)
print(bttfB)

et observez la différence de résultat pour les deux print : le premier ignore notre fonction description et affiche le descriptif par défaut de l'instance : normal, on n'a pas appelé la fonction. Le deuxième affiche notre description alors qu'on ne l'a pas demandé non plus… normal aussi, puisque c'est la conformance au protocole qui créé ce comportement !

De manière “magique”, ça a le même effet de bord pour l'interpolation dans une String :

print("I love \(bttfA)")
print("I love \(bttfB)")

Le premier print va donner :

I love MovieA(title: “Back to the Future”, year: 1985)

et le deuxième :

I love Back to the Future (1985)

Tutoriel, dernière partie

N'oublions pas de faire un array de descriptions :

extension CollectionType where Generator.Element: CustomStringConvertible {
    var descriptions:[String] {
        return self.map { $0.description }
    }
}

Le bloc précédent se lit :

“Dans une collection d'objets qui se conforment au protocole CustomStringConvertible, il y aura une propriété ‘descriptions’ qui renverra un array d'objets de type String, ces objets provenant d'une itération sur la collection pendant laquelle on retient la propriété ‘description’ de chaque objet”.

Bon, du concret !

Créons un array de Movies pour la suite de l'exercice :

let movies = [
    Movie(title: "Ghostbusters", year: 1984),
    Movie(title: "Gremlins", year: 1984),
    Movie(title: "Aliens", year: 1986),
    Movie(title: "Bram Stoker's Dracula", year: 1992),
    Movie(title: "Beverly Hills Cop", year: 1984),
    Movie(title: "2001, A Space Odyssey", year: 1968),
    Movie(title: "Basic Instinct", year: 1992),
    Movie(title: "X-Men", year: 2000),
    Movie(title: "Gladiator", year: 2000),
    Movie(title: "The Exorcist", year: 1973)
]

Et affichons leurs descriptions :

print(movies.descriptions)

Résultat :

“Ghostbusters (1984)”, “Gremlins (1984)”, “Aliens (1986)”, “Bram Stoker\’s Dracula (1992)”, “Beverly Hills Cop (1984)”, “2001, A Space Odyssey (1968)”, “Basic Instinct (1992)”, “X-Men (2000)”, “Gladiator (2000)”, “The Exorcist (1973)”

Profitons-en pour créer une propriété supplémentaire :

extension CollectionType where Generator.Element == Movie {
    var years:[Int] {
        return self.map { $0.year }
    }
}

Ce qui nous permet de faire :

let uniqueYears = movies.years.uniques.sort()
let yearsList = Array(uniqueYears.dropLast()).map { String($0) }.joinWithSeparator(", ")
print("Ces \(movies.count) films sont sortis en \(yearsList) et \(uniqueYears.last!).")

Résultat :

Ces 10 films sont sortis en 1968, 1973, 1984, 1986, 1992 et 2000.

Ajoutons encore quelques extensions pour un dernier développement :

extension CollectionType where Generator.Element == Movie {

    func before(year:Int) -> [Movie] {
        return self.filter { $0.year < year }
    }

    var byYear:[Movie] {
        return self.sort { $0.year < $1.year }
    }

}

let year = 1986
let moviesBefore = movies.before(year)
let list1986 = moviesBefore.byYear.descriptions.joinWithSeparator("\n")
print("Parmi eux, \(moviesBefore.count) sont d'avant \(year):\n\(list1986)")

Résultat :

Parmi eux, 5 sont d'avant 1986:
2001, A Space Odyssey (1968)
The Exorcist (1973)
Ghostbusters (1984)
Gremlins (1984)
Beverly Hills Cop (1984)

Code et Playground

Voici le code de cet article au complet.

C'est également disponible sous forme de Playground.

extension String {

    var letters:[String] {
        return self.characters.map { String($0) }
    }

}

let hello = "Hello world!"
print(hello.letters)

extension Int {

    var digits:[Int] {
        return String(self).letters.flatMap { Int($0) }
    }

}

let number = 314
print(number.digits)

extension String {

    var digits:[Int] {
        return self.letters.flatMap { Int($0) }
    }

}

let code = "X4Z3-82b"
print(code.digits)

extension String {

    var withoutDigits:[String] {
        return self.letters.filter { Int($0) == nil }
    }

}

print(code.withoutDigits)

extension String {

    var acronym:String {
        return self.withoutDigits.filter { $0 != " " }.filter { $0 == $0.uppercaseString }.joinWithSeparator("")
    }

}

let title = "Industrial Light and Magic 1992"
print(title.acronym)

extension CollectionType where Generator.Element == String {

    var arraysOfLetters:[[String]] {
        return self.flatMap { $0.letters }
    }

    var letters:[String] {
        return Array(self.arraysOfLetters.flatten())
    }

}

let words = ["Hello", "world!"]
print(words.arraysOfLetters)
print(words.letters)

print("---")

struct Movie: CustomStringConvertible {

    var title:String
    var year:Int

}

extension Movie {
    var description:String {
        return "\(title) (\(year))"
    }
}

extension CollectionType where Generator.Element: CustomStringConvertible {
    var descriptions:[String] {
        return self.map { $0.description }
    }
}

extension CollectionType where Generator.Element == Int {

    var uniques:[Int] {
        return Array(Set(self))
    }

}

extension CollectionType where Generator.Element == Movie {

    var years:[Int] {
        return self.map { $0.year }
    }

    var titles:[String] {
        return self.map { $0.title }
    }

    func from(year:Int) -> [Movie] {
        return self.filter { $0.year == year }
    }

    func before(year:Int) -> [Movie] {
        return self.filter { $0.year < year }
    }

    var byYear:[Movie] {
        return self.sort { $0.year < $1.year }
    }

}

let movies = [
    Movie(title: "Ghostbusters", year: 1984),
    Movie(title: "Gremlins", year: 1984),
    Movie(title: "Aliens", year: 1986),
    Movie(title: "Bram Stoker's Dracula", year: 1992),
    Movie(title: "Beverly Hills Cop", year: 1984),
    Movie(title: "2001, A Space Odyssey", year: 1968),
    Movie(title: "Basic Instinct", year: 1992),
    Movie(title: "X-Men", year: 2000),
    Movie(title: "Gladiator", year: 2000),
    Movie(title: "The Exorcist", year: 1973)
]

let uniqueYears = movies.years.uniques.sort()
let yearsList = Array(uniqueYears.dropLast()).map { String($0) }.joinWithSeparator(", ")
print("Ces \(movies.count) films sont sortis en \(yearsList) et \(uniqueYears.last!).")

let year = 1986
let moviesBefore = movies.before(year)
let list1986 = moviesBefore.byYear.descriptions.joinWithSeparator("\n")
print("Parmi eux, \(moviesBefore.count) sont d'avant \(year):\n\(list1986)")

print("---")

struct MovieA {

    var title:String
    var year:Int

    var description:String {
        return "\(title) (\(year))"
    }

}

let bttfA = MovieA(title: "Back to the Future", year: 1985)
print(bttfA)


struct MovieB: CustomStringConvertible {

    var title:String
    var year:Int

    var description:String {
        return "\(title) (\(year))"
    }

}

let bttfB = MovieB(title: "Back to the Future", year: 1985)
print(bttfB)

print("I love \(bttfA)")
print("I love \(bttfB)")
Auteur: Eric Dejonckheere