title | author | translator | category | excerpt | status | ||
---|---|---|---|---|---|---|---|
Never |
Mattt |
Vincent Pradeilles |
Swift |
Affirmer que quelque chose n'aura jamais lieu peut ressembler à inviter l'univers à prouver le contraire. Heureusement pour nous, Swift sait se prémunir contre cette éventualité grâce à un type des plus improbables : `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 {}
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.
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 commefatalError
, maisNever
est également très utile quand on manipule des classes génériques. Par exemple, un typeResult
pourrait utiliserNever
commeValue
pour représenter quelque chose qui produit systématiquement une erreur ou utiliserNever
commeError
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 !
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...
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()
}
}
{% info do %}
Dans les motivations derrière SE-0217: Introducing the !! "Unwrap or Die" operator to the Swift Standard Library, Joe Groff indique que "[...] Nous avons remarqué que surcharger [?? pour Never] avait un impact inacceptable sur les performances du vérificateur de types...". Ainsi, il est recommandé de ne pas introduire cet ajout dans votre code.
{% endinfo %}
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
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
.