Never

“Jamais” (en anglais, “Never”) est une indication qu’un évènement n’a pas lieu, à aucun moment du passé ou du futur. C’est une impossibilité logique sur la flèche du temps ; un vide qui s’étire dans toutes les directions, pour toujours.

…c’est pourquoi il est particulièrement préoccupant de rencontrer ce commentaire dans un code :

// ceci n'aura jamais lieu

Tous les manuels de compilateurs vous expliqueront qu’un tel commentaire ne peut et ne va pas affecter le comportement d’un code compilé. La Loi de Murphy n’est pas du même avis.

Comment Swift parvient-il à être sûr dans l’imprédictible chaos qu’est la programmation ? La réponse va vous surprendre : en ne faisant rien et en provoquant des crashs.


Never fût proposé comme remplacement pour l’attribut @noreturn dans la proposition d’évolution SE-0102: “Remove @noreturn attribute and introduce an empty Never type” de Joe Groff.

Antérieurement à Swift 3, les fonctions qui mettent fin à l’exécution, comme fatalError(_:file:line:), abort(), et exit(_:), étaient annotées avec l’attribut @noreturn, qui indiquait au compilateur qu’il n’y aurait pas de retour à l’appelant.

// Swift < 3.0
@noreturn func fatalError(_ message: () -> String = String(),
                               file: StaticString = #file,
                               line: UInt = #line)

Après la modification, fatalError et consorts déclarent retourner le type Never :

// Swift >= 3.0
func fatalError(_ message: @autoclosure () -> String = String(),
                     file: StaticString = #file,
                     line: UInt = #line) -> Never

Pour qu’un type soit capable de remplacer une annotation, il doit se révéler plutôt complexe ? Non ! C’est en fait l’opposé — Never est peut être le type le plus simple de toute la librairie standard Swift :

enum Never {}

Types Inhabités

Never est un type inhabité, c’est à dire qu’il ne possède pas de valeurs. Ou bien, pour le dire autrement, un type inhabité ne peut pas être construit.

Des énumérations qui ne possèdent aucun cas sont l’exemple le plus courant de types inhabités en Swift. À la différence des structures et des classes, les énumérations ne reçoivent pas de constructeurs. Et contrairement aux protocoles, les énumérations sont des types concrets, qui peuvent posséder des propriétés, méthodes, contraintes génériques, et types imbriqués. À cause de cela, les énumérations vides sont utilisées à travers Swift, pour implémenter des concepts tels que des namespaces et des fonctionnalités génériques.

Mais Never ne fait pas parti de ceux-ci. Il ne possède pas de fonctionnalités clinquantes. C’est son contenu lui-même (ou plutôt, son absence) qui le rend spécial.

Considérez une fonction déclarant retourner un type inhabité : puisque les types inhabités ne possèdent pas de valeurs, il est impossible à cette fonction de retourner normalement. (Comment cela serait-il possible ?) À la place, cette fonction doit, soit mettre fin à l’exécution, soit s’exécuter indéfiniment.

Supprimer des états impossible dans les types génériques

Bien sûr, cela est intéressant d’un point de vue théorique, mais quelle utilisation pratique peut-on faire de Never ?

Pas grand chose — ou du moins avant l’acceptation de la proposition d’évolution SE-0215: Conform Never to Equatable and Hashable

Dans cette proposition, Matt Diephouse explique que la motivation derrière l’implémentation d’Equatable et d’autres protocoles de cette façon :

Never est très utile pour représenter un chemin de code impossible. La plupart des gens se sont familiarisés avec lui via des fonctions comme fatalError, mais Never est également très utile quand on manipule des classes génériques. Par exemple, un type Result pourrait utiliser Never comme Value pour représenter quelque chose qui produit systématiquement une erreur ou utiliser Never comme Error pour représenter quelque chose qui ne produit jamais d’erreur.

Swift ne possède pas de type Result standard, mais la plupart d’entre-eux ressemble à ceci :

enum Result<Value, Error> {
    case success(Value)
    case failure(Error)
}

Les types Result sont utilisés pour encapsuler les valeurs et erreurs produites par des fonctions qui s’exécutent de façon asynchrone (alors que des fonctions synchrones peuvent utiliser throws pour transmettre des erreurs).

Par exemple, une fonction qui réalise une requête HTTP pourrait utiliser un Result pour encapsuler, soit une réponse et des données, soit une erreur :

func fetch(_ request: URLRequest,
          completion: (Result<(URLResponse, Data), Error>) -> Void) {
    // ...
}

À l’appel de cette méthode, il faudrait réaliser un switch sur son result pour gérer séparément les cas de .success et .failure :

fetch(request) { result in
    switch result {
    case let .success(response, _):
        print("Success: \(response)")
    case .failure(let error):
        print("Failure: \(error)")
    }
}

Considérons maintenant une fonction qui garanti de toujours retourner un succès dans sa fonction de rappel :

func alwaysSucceeds(_ completion: (Result<String, Never>) -> Void) {
    completion(.success("yes!"))
}

En indiquant Never comme type d’Error, nous utilisons le système de types pour indiquer qu’il n’est pas possible au traitement d’échouer. Ce qui est vraiment sympathique, c’est que Swift est suffisamment malin pour réaliser qu’il n’y a pas besoin de gérer le cas .failure pour que l’instruction switch soit exhaustive :

alwaysSucceeds { (result) in
    switch result {
    case .success(let string):
        print(string)
    }
}

Vous pouvez observer ce mécanisme poussé à son extrême dans l’implémentation permettant à Never de se conformer à Comparable :

extension Never: Comparable {
  public static func < (lhs: Never, rhs: Never) -> Bool {
    switch (lhs, rhs) {}
  }
}

Puisque Never est un type inhabité, il ne possède aucune valeur. Donc lorsque l’on réalise un switch sur lhs et rhs, Swift comprend qu’aucun cas possible n’est manquant. Et puisque tous les cas — qui sont simplement au nombre de zéro — retournent un booléen, la méthode compile sans aucun problème.

Habile !


Never comme un Type Zéro

En corollaire, la proposition d’évolution originale pour Never, fait allusion aux intérêts théoriques de ce type avec quelque améliorations supplémentaires :

Un type inhabité peut être vu comme un sous-type de n’importe quel autre type — si l’évaluation d’une expression ne produit jamais de valeur, peut importe le type de cette expression. Si cela était pris en charge pas le compilateur, cela permettrait des choses potentiellement utiles…

Unwrap ou meurt

L’opérateur force unwrap (!) est une des parties les plus controversées de Swift. Au mieux, il s’agit d’un mal nécessaire. Au pire, il est le signe d’un manque de rigueur. Et sans information supplémentaire, il peut être difficile de faire la différence entre les deux.

Par exemple, considérez le code suivant, qui suppose qu’un array n’est pas vide :

let array: [Int]
let firstIem = array.first!

Pour éviter un force unwrap, vous pourriez utiliser à la place une instruction guard avec une affectation conditionnelle :

let array: [Int]
guard let firstItem = array.first else {
    fatalError("array cannot be empty")
}

Dans le futur, si Never est implémenté comme un type zéro, il pourrait être utilisé dans le membre droit d’un opérateur ??.

// Future Swift? 🔮
let firstItem = array.first ?? fatalError("array cannot be empty")

Si vous êtes vraiment motivés pour adopter ce fonctionnement aujourd’hui, vous pouvez manuellement surcharger l’opérateur ?? de cette façon (toutefois…) :

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Never) -> T {
    switch lhs {
    case let value?:
        return value
    case nil:
        rhs()
    }
}

Throw comme expression

Similairement, si throw est modifié pour ne plus être une instruction mais une expression qui retourne Never, vous pourriez utiliser throw dans le membre droit de ?? :

// Future Swift? 🔮
let firstItem = array.first ?? throw Error.empty

Throws typé

En nous aventurant encore plus loin : si le mot-clé throws dans une déclaration de fonction supportait l’ajout de contraintes génériques, alors le type Never pourrait indiquer le fait q’une fonction ne génère pas d’erreur (d’une manière similaire à celle de Result) :

// Future Swift? 🔮
func neverThrows() throws<Never> {
    // ...
}

neverThrows() // pas besoin d'un `try` car la réussite est garantie

Affirmer que quelque chose n’aura jamais lieu peut ressembler à inviter l’univers à prouver le contraire. Alors que les logiques modales ou doxastiques sauvent la face en adoptant un compromis (“cela était vrai à un moment, ou du moins je l’ai cru !”), la logique temporelle impose un plus haut niveau d’exigence à ses propositions.

Heureusement pour nous, Swift s’impose également les mêmes garanties, grâce à un type des plus improbables : Never.

Écrit par Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon. He is the founder of NSHipster and Flight School, and the creator of several open source libraries, including AFNetworking and Alamofire.

Traduit par Vincent Pradeilles
Vincent Pradeilles

Vincent Pradeilles est dévelopeur iOS chez Worldline à Lyon. Il collabore à NSHipster en tant que traducteur français.

Article suivant

Notre sujet de cette semaine est le protocol Hashable et le nouveau type qui lui est lié : Hasher. À eux deux, ils comprennent les fonctionnalités sous-jacentes à deux des collections les plus appréciées de Swift : Dictionnary et Set.