Funktionen |
Wir haben schon eigene Funktionen mit "defn" definiert:
(defn greet [name] (str "Hallo, " name))
(greet "Welt")
((fn [name] (str "Hallo, " name)) "Welt")
"defn" = "def" + "fn"
(def greet (fn [name] (str "Hallo, " name)))
(greet "Welt")
Funktionen können genauso als Werte verwendet werden, wie andere Werte. Sie unterscheiden sich in der Hinsicht nicht von einer Zahl oder einem String.
(def operations {:greet (fn [name] (str "Hallo, " name))
:farewell (fn [name] (str "Tschüß, " name))})
((get operations :farewell) "Welt")
Dass wir eine Funktion als Wert verwenden können, verwundert eigentlich nicht.
Eine Funktion ist ja auch nur eine Liste.
Prinzip: "Data as code"
Im Kapitel über Macros wird das ein größeres Thema sein.
Nur echte Funktionen können als Werte verwendet werden:
{:and and
:if if}
"if" ist eine "Special Form" und "and" ist ein Macro.
Mit Ausnahme von Special Forms und Macros behandelt Clojure alle Funktionen gleich.
Das Eingebaute "+" unterscheidet sich nicht von einer eigenen Funktion "plus".
Wenn Funktionen Werte sind, können wir sie auch als Parameter an andere Funktionen übergeben:
(defn apply-operation [op] (op 1 2 3 4))
(apply-operation +)
; => 10
(apply-operation -)
; => -8
(apply-operation *)
; => 24
Funktionen, welche andere Funktionen als Parameter nehmen, nennen wir "Higher Order Functions".
Das ist eines der Kern Features von funktionalen Programmiersprachen.
Konsequenterweise können wir auch Funktionen als Rückgabewert haben:
(defn make-incrementer [inc-by]
(fn [x] (+ x inc-by)))
(def inc-4 (make-incrementer 4))
(inc-4 7)
Für sehr kleine anonyme Funktionen gibt es eine Kurzschreibweise:
(defn make-incrementer-2 [inc-by]
#(+ % inc-by))
((make-incrementer-2 4) 7)
Diese Notation wird sehr schnell unleserlich und sollte auf kleine Funktionen begrenzt bleiben.
Die Kurzschreibweise erlaubt es auch, die Parameter zu unterscheiden:
(def announce-winners
#(str "On the second place is " %2 " while the winner is " %1))
(announce-winners "Alice" "Bob")
Nochmal die Warnung: Solche Funktionen nur einsetzen, wenn sie sehr kurz sind.
Als wir die "inc-4" Funktion erstellt haben ist etwas besonderes passiert. Die Funktion hat sich "inc-by" gemerkt:
(defn make-incrementer [inc-by]
(fn [x] (+ x inc-by)))
(def inc-4 (make-incrementer 4))
(inc-4 7)
"inc-by" ist Teil der "Closure" von "inc-4" geworden.
Funktionen haben Zugriff auf Werte, auf die sie bei der Definition Zugriff hatten.
Da alle Werte immutable sind, brauchen wir uns keine Sorgen zu machen, dass sich die Closure später ändern könnte.
Unser "make-incrementer" ist ein Beispiel für "Currying" (oder "Schönfinkeln").
Parameter einer allgemeineren Funktion durch Currying zu fixieren ist ein gängiges Pattern.
Wir erhalten eine "partially applied function".
Mit partial gibt es eine Standardfunktion dafür
(def inc-4 (partial + 4))
Funktionen können als Rückgabewerte genauso Informationen transportieren wie andere Datenstrukturen:
(defn make-person [name age]
(fn [m] (m name age)))
(defn get-name [person]
(person (fn [name age] name)))
(def john (make-person "john" 27))
(get-name john)
"Code as data"
Wir haben gesehen, dass wir Daten als Code und Code als Daten interpretieren können.
Das gilt für fast alle Programmiersprachen, ist aber in der LISP Familie besonders prägnant.
Für alle, denen jetzt noch nicht der Kopf schwirrt, gibt es den Lambda Calculus.
(defn zero [f] (fn [x] x))
(defn add-1 [n] (fn [f] (fn [x] (f ((n f) x)))))
Jede nicht negative ganze Zahl entspricht einer Funktion (Church Numerals).
Bei der Definition von Funktionen gibt es noch ein paar mehr Features
Diese machen die Sprache nicht mächtiger, sind aber nützlich.
Docstring
(defn make-incrementer
"Creates a function that increments by a given number"
[inc-by]
#(+ % inc-by))
(doc make-incrementer)
Wir können unterschiedliche Funktionen für verschiedene Anzahlen an Parametern definieren:
(defn multi-arity
([] "no parameters")
([x] (str "parameter " x))
([x y] (str "parameters " x " and " y)))
(multi-arity 5)
; => "parameter 5"
Das ist auch ein Weg, um Default Werte für Parameter festzulegen.
Funktionen können eine variable Menge an Parametern haben:
(defn greeter [formula & names]
(if (= (count names) 0)
(println formula "to no one")
(do
(println formula (nth names 0))
(if (> (count names) 1)
(println formula "Rest")))))
(greeter "Hello" "Marie" "Paul")
Nach dem "&" kann noch genau ein Parameter kommen, der den Rest enthält.
Das Konzept, eine Datenstruktur einfach zu zerlegen nennt sich "Destructuring".
Destructuring funktioniert auch bei einzelnen Parametern
(defn greeter [formula [first-name & other-names]]
(println formula first-name)
(if (> (count other-names) 0)
(println formula "Rest")))
(greeter "Hello" ["Marie" "Paul"])
(unser Function Body kann mehr als eine Expression enthalten - auch ohne "do")
Destructuring funktioniert auch bei Maps
(defn announce-location
[{latitude :lat
longitude :lng}]
(println "Treasure is at lat:" latitude
"and lng:" longitude))
(announce-location {:lat 12.4 :lng 23.1})
Mit Destructuring, wird unser Code meist lesbarer und einfacher.