Publié par

Il y a 3 mois -

Temps de lecture 4 minutes

Pépite #23 – Opaque return types

***Cette pépite est la troisième d’une série de 3. Elles forment la base du classroom « From Existential to Opaque return types » qui s’est tenu pendant la FrenchKit 2019***

Qu’est ce qu’un Opaque Return Type ?

Un type opaque permet de ne pas exposer le type concret qui se conforme à un protocole tout en donnant accès au compilateur à l’information du type concret.

Ceci est utile dans les cas où nous avons un PAT (protocol associated type) ou quand une des fonctions du protocole utilise Self comme contrainte, vu que dans les 2 cas l’implémentation dépend du type concret qui se conforme au protocole.

Jusqu’à Swift 5.0

Avant Swift 5.0, nous pouvions implémenter un protocole, mais nous perdions alors toute information concernant le type concret derrière et dans le cas où cela concernait un PAT comme par exemple le protocole Container :

protocol Container {
   associatedtype Item
}

cela générait des erreurs de compilation comme les suivantes :

Ces erreurs sont produites car le compilateur n’a pas assez d’information en ce qui concerne l’associated type de notre PAT.

Qu’est ce que Swift 5.1 change ?

À partir de Swift 5.1, les fonctions, les computed properties et les subscripts peuvent retourner un type opaque. Cela est possible grâce au nouveau mot-clé some.
Il suffit de retourner le protocole comme cela :

protocol Animal {
    associatedType AnimalSound
}

func getBestAnimal -> some Animal {
    ...
}

Ce nouveau mot-clé some nous évite de partager les détails de l’implémentation du protocole et il nous permet d’utiliser un PAT comme type de retour, chose que l’on ne pouvait pas faire jusqu’à présent.
Maintenant, le compilateur connait le type concret derrière notre protocole et dans le cas de notre PAT, il connait son associated type.

Tout cela permet à l’utilisateur de la fonction getBestAnimal de ne pas se préoccuper de l’implémentation de cette fonction ni du type concret derrière le protocole. Son code compilera même si un jour le meilleur animal devient un chien et l’implémentation de getBestAnimal change.

Et si on voulait que le meilleur animal soit parfois un chat et parfois un chien ? Est-ce que la fonction getBestAnimal peut retourner parfois un chat et parfois un chien ?

La réponse est non : le compilateur vous dira que cette fonction ne retourne pas toujours le même type concret. Cependant, on peut utiliser une technique appelée Type Erasure, dont on a deja parlé dans cette pépite : Type-Erasure en Swift. SwiftUI utilise notamment cette technique.

SwiftUI

SwiftUI est le framework tout beau tout nouveau qui nous permet de construire notre interface utilisateur de façon déclarative. Un des principes fondamentaux de SwiftUI est précisément les opaque return types, parce que le type qui représente une view dans SwiftUI est un PAT. Voici une version simplifiée de ce protocole :

protocol View {
    associatedtype Body: View

    var body: Self.Body { get }
}

 

Et la façon dont SwiftUI utilise ce protocole et le mot-clé some :

class MyView: View {
	var body: some View {
		Text("Hello World")
	}
}

Vu que body est de type some View, on peut changer facilement le type concret qui est derrière, c’est-à-dire, changer un Text() par une Image(), sans problème de compilation.

Comme indiqué précédemment, SwiftUI utilise la technique de Type erasure avec un Struct appelé AnyView. Ceci est fait dans le but de pouvoir définir pendant l’exécution du code le type concret derrière le protocole View comme dans cet exemple :

var body: some View {
	if Bool.random() {
		return AnyView(Text(""))
	} else {
		return AnyView(Image("myImage"))
	}
}

Opaque return types est-il la solution à tous nos problèmes ?


La réponse est malheureusement « non » : comme son nom l’indique celle-ci nous permet seulement de retourner un existential mais pas de les utiliser comme argument dans une fonction ou comme contrainte. Ce dont nous avons besoin ce sont des « Generalized Existentials » qui nous permettraient d’écrire :

protocol IteratorProtocol {
  associatedtype Element
  mutating func next() -> Element?
}

let it: IteratorProtocol = ...
it.next()

ou

let strings: Any<Sequence where .Iterator.Element == String> = ["a", "b", "c"]

Si nous sommes chanceux, un jour nous aurons des « generalized existentials »… et si nous sommes vraiment chanceux nous aurons même une nouvelle pépite pour en parler.

Publié par

Publié par Patricio Guzman

Développeur iOS chez Xebia. Il aime le code propre et testable.

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.