Swift 5.9 bêta : le langage s'enrichit de macros

Par:
fredericmazue

mar, 04/07/2023 - 13:04

Swift 5.9, qui vient de sortir en version bêta au moment où nous écrivons ces lignes, a été présenté lors de la conférence WWDC 2023. Cette nouvelle version du langage vient avec une nouveauté majeure : le support de macros.

Dans tous les langages qui en disposent, les macros permettent d'éviter d'écrire du code répétitif. Lors de la phase se compilation, le compilateur étend la macro, c'est-à-dire ajoute le code apporté par celle-ci au code existant, puis compile l'ensemble. Il en va de même avec Swift.

Swift a deux types de macros :

  • Les macros autonomes (standalone) qui apparaissent seules, sans être attachées à une déclaration.
  • Les macros attachées qui modifient la déclaration à laquelle elles sont attachées.

Macro autonome

Voici un exemple d'utilisation de macro autonome :

func myFunction() {
    print("Currently running \(#function)")
    #warning("Something's wrong")
}

Dans cet exemple, function est une macro de la bibliothèque standard Swift. Lorsque vous compilez ce code, Swift étend cette macro, ce qui a pour effet de remplacer function par le nom de la fonction actuelle. A l'exécution ce code imprime "Currently running myFunction()". A la ligne suivante, la macro warning(_:),  de la bibliothèque standard Swift produit un avertissement personnalisé au moment de la compilation.

Macro attachée

Pour appeler une macro attachée, vous écrivez un arobase (@) avant son nom et vous écrivez tous les arguments de la macro entre parenthèses après son nom.

Les macros attachées modifient la déclaration à laquelle elles sont attachées. Ils ajoutent du code à cette déclaration, comme la définition d'une nouvelle méthode ou l'ajout de conformité à un protocole.

Par exemple, considérez le code suivant qui n'utilise pas de macros :

struct SundaeToppings: OptionSet {
    let rawValue: Int
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

Dans ce code, chacune des options du jeu d'options inclut un appel à l'initialiseur, qui est répétitif 

Voici une version de ce code qui utilise une macro à la place :

@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

Cette version utilise la macro @OpentionSet de la librairie standard de Swift. La macro lit la liste des cas dans l'énumération privée, génère la liste des constantes pour chaque option et ajoute une conformité au protocole.

Voici à quoi ressemble la version étendue de la macro.

struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }

    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppings: OptionSet { }

Tout le code après l'énumération privée provient de la macro.

Les macros de Swift 5.9 sont documentées ici. Cette documentation montre comme utiliser les macros de Swift, mais aussi, bien sûr, comment déclarer vos propres macros.