Eine der weniger bekannten Funktionen von Swift ist Function Currying. Wenn Sie das Swift-Sprachhandbuch lesen, werden Sie nichts über Curry-Funktionen finden. Apple beschreibt sie nur in der Swift-Sprachreferenz. Und das ist schade, denn es ist eine sehr leistungsfähige und nützliche Funktion, die mehr Aufmerksamkeit verdient. In diesem Beitrag geht es um die Grundlagen und einige Szenarien, in denen die Verwendung von Curried Functions nützlich sein kann.
Ich gehe davon aus, dass Sie mit dem Currying von Funktionen bereits etwas vertraut sind, da es in vielen anderen Sprachen existiert. Falls nicht, gibt es im Internet viele Artikel, die erklären, was es ist und wie es funktioniert. Kurz gesagt: Sie haben eine Funktion, die einen oder mehrere Parameter empfängt. Dann wenden Sie einen oder mehrere bekannte Parameter auf diese Funktion an, ohne sie bereits auszuführen. Danach erhalten Sie einen Funktionsverweis auf eine neue Funktion, die die ursprüngliche Funktion mit den angewendeten Parametern aufruft.
[objc]
func doGET(url: String, completionHandler: ([String]?, NSError?) -> ()) {
// do a GET HTTP request and call the completion handler when receiving the response
}
[/objc]
Dies ist ein ziemlich gängiges Muster, das Sie auch bei den meisten Netzwerkbibliotheken finden. Wir können es mit einer Url aufrufen und im Completion Handler eine Reihe von Dingen tun:
[objc]
doGET("https://someurl.com/items?all=true", completionHandler: { results, error in
self.results = results
self.resultLabel.text = "Got all items"
self.tableView.reloadData()
})
[/objc]
Der Completion Handler kann sehr viel komplexer werden und Sie möchten ihn vielleicht an verschiedenen Stellen wiederverwenden. Daher können Sie diese Logik in eine separate Funktion auslagern. Zum Glück sind Funktionen in Swift nur Closures, so dass wir der doGET-Funktion sofort eine Funktion für die Abschlussbehandlung übergeben können:
[objc]
func completionHandler(results: [String]?, error: NSError?) {
self.results = results
self.resultLabel.text = "Got all items"
self.tableView.reloadData()
}
func getAll() {
doGET("https://someurl.com/items?all=true", completionHandler)
}
func search(search: String) {
doGET("https://someurl.com/items?q=" + search, completionHandler)
}
[/objc]
Das funktioniert gut, solange der Completion Handler immer genau das Gleiche tut. Aber in der Realität ist das normalerweise nicht der Fall. Im obigen Beispiel wird im resultLabel immer "Got all items" angezeigt. Ändern wir dies in "Got searched items" für die Suchanfrage:
[objc]
func search(search: String) {
doGET("https://someurl.com/items?q=" + search, {results, error in
self.completionHandler(results, error: error)
self.resultLabel.text = "Got searched items"
})
}
[/objc]
Das wird funktionieren, sieht aber nicht sehr schön aus. Was wir eigentlich wollen, ist dieses dynamische Verhalten in der Funktion completionHandler. Wir können den completionHandler so ändern, dass er den Text für das resultLabel als Parameter akzeptiert und dann den eigentlichen completionHandler als Closure zurückgibt.
[objc]
func completionHandler(text: String) -> ([String]?, NSError?) -> () {
return {results, error in
self.results = results
self.resultLabel.text = text
self.tableView.reloadData()
}
}
func getAll() {
doGET("https://someurl.com/items?all=true", completionHandler("Got all items"))
}
func search(search: String) {
doGET("https://someurl.com/items?q=" + search, completionHandler("Got searched items"))
}
[/objc]
Und wie sich herausstellt, ist dies genau das, was wir auch mit Currying tun können. Wir müssen nur die Parameter des eigentlichen Completion Handlers als zweite Parametergruppe zu unserer Funktion hinzufügen:
[objc]
func completionHandler(text: String)(results: [String]?, error: NSError?) {
self.results = results
self.resultLabel.text = text
self.tableView.reloadData()
}
[/objc]
Wenn Sie diese Funktion mit dem ersten Textparameter aufrufen, wird sie noch nicht ausgeführt. Stattdessen wird eine neue Funktion mit [String]?, NSError? als Parameter zurückgegeben. Sobald diese Funktion aufgerufen wird, wird die Funktion completionHandler schließlich ausgeführt. Sie können so viele Ebenen dieses Currys erstellen, wie Sie möchten. Und Sie können auch die letzte Parametergruppe leer lassen, nur um einen Verweis auf die vollständig ausgeführte Funktion zu erhalten. Schauen wir uns ein anderes Beispiel an. Wir haben eine einfache Funktion, die den Text des resultLabel setzt:
[objc]
func setResultLabelText(text: String) {
resultLabel.text = text
}
[/objc]
Und aus irgendeinem Grund müssen wir diese Methode asynchron aufrufen. Das können wir mit den Grand Central Dispatch-Funktionen tun:
[objc]
dispatch_async(dispatch_get_main_queue(), {
self.setResultLabelText("Some text")
})
[/objc]
Da die Funktion dispatch_async nur eine Closure ohne Parameter akzeptiert, müssen wir hier eine innere Closure erstellen. Wenn setResultLabelText eine Curried-Funktion wäre, könnten wir sie vollständig mit dem Parameter anwenden und einen Verweis auf eine Funktion ohne Parameter erhalten:
[objc]
func setResultLabelText(text: String)() { // now curried
resultLabel.text = text
}
dispatch_async(dispatch_get_main_queue(), setResultLabelText("Some text"))
[/objc]
Aber möglicherweise haben Sie nicht immer die Kontrolle über solche Funktionen, zum Beispiel wenn Sie Bibliotheken von Drittanbietern verwenden. In diesem Fall können Sie die ursprüngliche Funktion nicht in eine Curried-Funktion ändern. Oder Sie möchten sie nicht ändern, da Sie sie bereits an vielen anderen Stellen verwenden und nichts kaputt machen wollen. In diesem Fall können wir etwas Ähnliches erreichen, indem wir eine Funktion erstellen, die die curried-Funktion für uns erzeugt:
[objc]
// defined in global scope
func curry<T>(f: (T) -> (), arg: T)() {
f(arg)
}
[/objc]
Wir können sie nun wie folgt verwenden:
[objc]
func setResultLabelText(text: String) {
resultLabel.text = text
}
dispatch_async(dispatch_get_main_queue(), curry(setResultLabelText, "Some text"))
[/objc]
Wahrscheinlich wäre es in diesem Beispiel genauso einfach, die innere Schließung zu verwenden, aber die Möglichkeit, teilweise angewandte Funktionen weiterzugeben, ist sehr leistungsfähig und wird bereits in vielen Programmiersprachen verwendet.
Leider zeigt das letzte Beispiel auch einen großen Nachteil der Art und Weise, wie Currys in Swift implementiert sind: Sie können nicht einfach normale Funktionen curren. Es wäre schön, wenn Sie jede Funktion, die mehrere Parameter annimmt, mit einem Curry versehen könnten, anstatt explizit Curry-Funktionen erstellen zu müssen. Ein weiterer Nachteil ist, dass Sie nur in der festgelegten Reihenfolge der Parameter curryen können. Sie können also keine umgekehrten Currys anwenden (z.B. nur den letzten Parameter) und auch nicht jeden beliebigen Parameter, unabhängig von seiner Position. Hoffentlich wird sich die Sprache Swift in dieser Hinsicht weiterentwickeln und leistungsfähigere Curry-Funktionen erhalten.
Verfasst von

Lammert Westerhoff
Contact



