Swift - la fonction: une closure particulière

Swift est un langage simple pour les débutants, tout en offrant de grandes possibilitées aux développeurs expérimentés. Les fonctions Swift sont un bon exemple de cette façon d’avoir pensé le langage.

#Les fonctions

#Déclaration simple

La déclaration et l’utilisation de fonctions est simple. On commence avec le mot clé func suivi du nom, puis des paramètres entre parenthèses (a.k.a. input) :

func sayHello(name:String, age:UInt){
    print("Hello \(name), you are \(age)")
}

L’appel se fait aussi simplement que ça : sayHello(name:"Bob", age: 32). Le mot clé _ permet d'enlever le label du paramètre dans l'appel si besoin :

// Declaration
func sayHello(name:String,_ age:UInt){
 print("Hello \(name), you are \(age)")
}
// Appel
sayHello(name:"Bob", 32)

Mais vous perdrez en clarté de code suivant les cas. Par exemple, avec min(3,6) on voit tout de suite ce que fait la fonction. Alors que sayHelloTo("Bob", 32), le 32 pourrait être beaucoup de choses.

Les paramètres sont obligatoirement typés, mais peuvent être optionnels (en autorisant la valeur nil) avec la notation ? (e.g. Int?, String?).

Vous pouvez aussi définir des valeurs par défauts pour les paramètres :

// Declaration
func sayHello(name:String, _ age:UInt? = nil){
    if age == nil {
        print("Hello \(name)")
    } else {
        print("Hello \(name), you are \(age!)") //! permet de pas afficher Optinal(value)
    }
}
// Appel
sayHello(name:"Bob") // -> Hello Bob
sayHello(name:"Bob", 32) // ->  Hello Bob, you are 32

La plupart du temps nos fonctions servent à nous retourner des informations. Pour cela on utilise -> en signe de retour suivi du type de la valeur retournée.

func formatHelloSentence(name:String, age:UInt? = nil) -> String {
    if age == nil {
        return "Hello \(name)"
    } else {
        return "Hello \(name), you are \(age)"
    }
}
// Appel
let helloSentence = formatHelloSentence(name:"Bob", age: 32)

#Retours multiples ou Tuples

Apple n’est pas connu pour sa générosité, mais dans Swift ils en ont fait autrement :troll: : on peut retourner plusieurs valeurs avec une seule fonction. Pour cela on utilise un Tuple : un collection de variables ordonnées.

func hardFunction(fInputs:[Float])->(Int,Int,Float,String) {    
    var a = 0
    var b = 0
    var f : Float = 0.0
    var str = ""
    //...
    return (a,b,f,str)
}

Ici cette fonction prend en paramètres un tableau de Float et retourne un Tuple composé de deux Int, un Float et un String dans cet ordre. Vous pouvez alors utiliser le Tuple comme une structure en utilisant l’index des éléments du Tuple comme nom de variable (ex: 0pour le premier Int, 3 pour le String).

let myTuple = hardFunction([2.4,2.6,1.8])
let myTupleString = myTuple.3

Mais ceci reste assez confus, alors on peut nommer les éléments.

func hardFunction(fInputs:[Float])->(valSup:Int,
        valMax:Int,
        average:Float,
        errorString:String) {
    //...
}

//utilisation
let myTuple = hardFunction([2.4,2.6,1.8])
let myTupleString = myTuple.errorString // myTuple.3 marche encore

L'exécution du code permettant d'avoir les valeurs Tuple n'est effectuée qu'à la demande de ces valeurs. Dans l'exemple ci-dessus, le code de hardFunction ne sera appelé qu'à la dernière ligne, car c'est là qu'on a besoin de errorString, pas avant.

Et la notion de Closure fait son entrée!

#La closure: la variable-fonction

#Quésaco

Une closure est une partie de code, avec paramètres et sorties, qui peut être encapsulé dans une variable, et exécutée à la demande. Si vous développez en C++ ou Objective-C vous connaissez peut-être déjà les blocks et en Java ou C# les lambdas qui sont des features très (très) proches.

#Déclaration

Une closure se déclare grâce au {} et peut être appelée (exécutée) grâce aux parenthèses :

 let helloClosure = {
     print("hello, I’m a closure")
 }

 helloClosure() // Le code est exécuté ici

Ça vous rappelle rien ? L’appel d’une fonction ! En réalité, la fonction est une closure particulière associé à un contexte (Object, environnement, Bundle...) pour réaliser des optimisations et une meilleur compréhension du code.

Pour une closure "à l’air libre", on dit d’elle, qu’elle est Self Contained alors qu’une fonction est contenue par un contexte (class par exemple)

Comme les fonctions, les closures ont des paramètres d’entrée et de retour:

let complexClosure = {(name:String, age:Float) -> Bool in
    // Code
    return false
}
let success = complexClosure("Louis",32)
print("Louis has \(success)")

Ainsi, les valeurs dans la première partie après la { sont les paramètres d’entrées et après la -> ce sont les paramètres de sortie. Le code à exécuter est après le in.

Comme vous avez dû le remarquer, les closures n’ont pas de paramètres nominatif. IL faut passer les paramètres d'entrée dans l'ordre de la déclaration.

#Closure et Type

Toute variable est typée en Swift, implicitement ou explicitement. Pour les closures le type est souvent implicite, aussi bien qu’on en oublie souvent qu’elles sont typées. Le type d’une closure va être défini par ses paramètres d’entrée et de sortie. Ainsi la complexClosure ci-dessus est du type : ((String, Float)) -> (Bool).

Je peux alors écrire ce code puisque les closures sont du même type :

let otherComplexClosure = { (surname:String,size:Float) -> Bool in
    // Other complexe Code
    return true
}
complexClosure = otherComplexClosure

#Utilisation du contexte

Les closures ont une connaissance du contexte qui l’entoure. Ce qui veut dire que si la closure est créé dans une méthode, elle aura accès :

  • au contexte de classe en passant par self (variables, autre fonctions...)
  • au contexte de la fonction (paramètres, fonctions internes...)
class Animal {
    var name = "Boby"

    func crier(cri:String){
        let uselessClosure = {
            print("\(self.name) cri \(cri)")
        }
        uselessClosure()
    }
}

#Trailing Closure

Pour finir, un peu d'esthétisme car on aime tous le beau code. La Trailing Closure est une syntaxe d'appel de fonction qui permet de rendre le code plus facile à lire.

Prenant la fonction suivante, prenant une URL est une closure de type Void->Void :

func doLongTask(on file:URL,
        completion:() -> ()){
    //long task
    completion()
}

On alors l'appeler cette fonction comme ceci :

doLongTask(on: aFileURL, completion:{
    print("Task Applied on \(aFileURL)")
})

Avec du code plus complexe, ça commence à devenir difficile à lire. Or si le dernier paramètre d'une fonction est une closure, on peut alors écrire l'appel comme ceci :

doLongTask(on: aFileURL){
    print("Task Applied on \(aFileURL)")
}

#Pro Tip

Imaginons une classe A ayant une variable event et un classe B ayant une fonction awesomeEvent ayant le même type que la variable event de la classe A. Je peux alors allouer, à la variable event, le code de awesomeEvent.

class A {
    var event : ((String)->Void)?
}

class B {
    func awesome(name:String){
        print("Awesome \(name)")
    }
}

let b = B()
let a = A()

a.event = b.awesome
a.event?("Mate")

Exécutez ce code sur IBM Swift Sandbox c'est magique!

#Conclusion

Voilà vous savez tout, ou presque, sur les fonctions et les closures. Les closures ont vraiment la part belle en Swift, donc apprenez à les utiliser et les comprendre pour simplifiez votre code. Elles sont partout dans les API iOS et macOS.

Et un petit conseil: faite attention à l'état de vos closures dans vos Thread si vous ne voulez pas de fuite 😊