Technisch gesehen ist eine Higher Order Function einfach eine Funktion, die mindestens eines von beidem tut:
In der Welt der funktionalen Programmierung ist die Verwendung meist enger gefasst.
Der Begriff bezeichnet meist Funktionen wie filter, map und reduce, die eine übergebene Funktion auf die Elemente einer Kollektion anwenden.
An diesem Punkt im Lehrplan haben wir bereits verschiedene Wege gesehen, eine Operation auf alle Elemente einer iterierbaren Kollektion wie einem Vector anzuwenden:
Dieses Konzept konzentriert sich auf Funktionen höherer Ordnung (bekannt aus jeder funktionalen Sprache wie Haskell oder F#).
Weitere mögliche Ansätze sind:
Die Funktion filter() nimmt eine übergebene Funktion mit einem booleschen Rückgabewert und wendet sie auf eine Kollektion an.
Nur Elemente, die true zurückgeben, sind im Rückgabewert enthalten, der denselben grundlegenden Typ wie die Eingabe hat (siehe unten).
julia> filter(iseven, 1:6)
3-element Vector{Int64}:
2
4
6
# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"
# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)
Bei mehrdimensionalen Arrays flacht filter die Dimensionen der Eingabe ab und gibt einen Vector zurück: die wichtigste Ausnahme von jeder Regel, nach der der Ausgabetyp zum Eingabetyp passt.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> filter(isodd, m)
3-element Vector{Int64}:
1
5
3
Die Beispiele oben verwenden eingebaute Funktionen, aber der Einsatz anonymer Funktionen ist in diesem Zusammenhang sehr verbreitet.
julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
3
6
9
12
15
18
Es gibt auch eine In-Place-Version, filter!(), wie für viele der Funktionen in diesem Konzept.
Die Funktion map() transformiert eine Kollektion, indem sie eine Funktion auf jedes Element anwendet.
In einfachen Fällen kann das ähnlich wie Broadcasting sein, wobei die Form der Ausgabe zur Eingabe passt.
julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
2
5
10
17
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> map(√, m)
2×3 Matrix{Float64}:
1.0 1.41421 1.73205
2.0 2.23607 2.44949
map() arbeitet auch elementweise auf mehreren Kollektionen.
julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
3
8
Konzeptionell können wir uns das so vorstellen, als würden wir zip() auf den mehreren Eingabekollektionen ausführen und dann map() auf jedes Element des Zwischenergebnisses anwenden.
Das ist nur eine grobe Analogie und sagt nichts über die Implementierung aus!
Wie bei zip() werden Kollektionen mit nicht zueinander passender Form auf die Dimension(en) der kleinsten gekürzt.
Manchmal werden nur die Seiteneffekte der übergebenen Funktion gebraucht, etwa ein Schreibvorgang in eine Datenbank oder ein push! in ein Array.
Dann steht die Funktion höherer Ordnung foreach() zur Verfügung, die immer nothing zurückgibt.
Die Funktion reduce() nimmt eine Funktion mit zwei Argumenten und wendet sie auf eine Kollektion an, was zu einer Dimensionsreduktion führt.
Das mag abstrakt verwirrend klingen, aber denk an Funktionen wie sum() oder prod(), die eine Kollektion entgegennehmen und einen einzelnen Wert zurückgeben.
julia> sum(1:4) # add
10
julia> prod(1:4) # multiply
24
Diese speziellen Funktionen sind hoch optimiert und sollten immer verwendet werden, wenn sie verfügbar sind.
Weitere Beispiele sind maximum() und minimum(), die logischen Funktionen all() und any() sowie viele statistische Funktionen.
Nur zur Veranschaulichung: betrachte dieselbe Funktionalität, umgesetzt mit dem allgemeineren reduce() (zur Erinnerung: die Infix-Operatoren + und * sind intern eigentlich Funktionen).
julia> reduce(+, 1:4) # add
10
julia> reduce(*, 1:4) # multiply
24
Wie sum() und andere Aggregationsfunktionen kann reduce() ein optionales Keyword-Argument dims annehmen, um die zu reduzierenden Dimension(en) anzugeben.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
5 7 9
Das sind einfache Beispiele, weil Addition und Multiplikation sowohl kommutativ (1+2 == 2+1) als auch assoziativ sind ( (1+2)+3 == 1+(2+3) ).
Das ist alles andere als universell! Selbst so gängige Operationen wie Subtraktion und Division sind nicht assoziativ.
Dazu kommt das Problem, dass sich Gleitkommafehler über große Kollektionen hinweg ansammeln können, sodass ein Reduce von links nach rechts ein etwas anderes Ergebnis liefern kann als von rechts nach links.
Die Richtung der reduce-Funktion von Julia ist implementierungsabhängig und nicht garantiert.
Um die Richtung explizit zu steuern, gibt es die Funktionen foldl() und foldr(), die nominell bei „links“ bzw. „rechts“ beginnen (bei einem Vector tatsächlich oben und unten).
julia> foldl(-, 1:3) # (1 - 2) - 3
-4
julia> foldr(-, 1:3) # 1 - (2 - 3)
2
Beachte, dass diese für Kollektionen gedacht sind, die sich als eindimensional behandeln lassen und ein skalares Ergebnis zurückgeben.
Ein dims-Argument wird für foldl und foldr nicht unterstützt, nur für reduce.
Eine map-Operation mit einem reduce zu kombinieren, ist in vielen Programmierbereichen sehr verbreitet.
Wir könnten nacheinander map ausführen und dann reduce auf eine Zwischenkollektion anwenden.
Das ist jedoch bestenfalls ineffizient und skaliert sehr schlecht, je größer die Kollektion wird.
Es wird dringend empfohlen, stattdessen die kombinierte Funktion mapreduce() zu verwenden.
Sie kann einen deutlich leistungsfähigeren Algorithmus umsetzen, der die map/reduce-Operationen ineinander verschränkt.
Das erste Argument ist die Funktion, mit der abgebildet wird, das zweite Argument ist der Reduce-Operator.
julia> mapreduce(x -> x^2 + 1, +, 1:3)
17
# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17
Wie zu erwarten, hat Julia auch mapfoldl()- und mapfoldr()-Funktionen für Situationen, in denen die Richtung wichtig ist.
Wir gründen einen Käseclub, der maschinelles Lernen nutzt, um auf Basis der Vorgeschichte und des Geschmacks unserer käseliebenden Kundinnen und Kunden neue Käsesorten für unser Angebot auszuwählen.
Neue Mitglieder müssen zu Beginn eine Umfrage ausfüllen, damit wir erste grundlegende Daten sammeln können. Dabei hat sich gezeigt, dass es eine Untergruppe emphatischer Kundinnen und Kunden gibt, deren Kritik nicht differenziert genug ist. Da das einen differenzierteren Algorithmus unwiderruflich verzerren kann, gibt es einen separaten Algorithmus, der ihre Bedürfnisse abdeckt. Du sollst einige Hilfsfunktionen bereitstellen, um ihre Daten zu bändigen.
Auch wenn es verschiedene Wege gibt, die folgenden Aufgaben zu lösen, lässt sich jede mit einer anderen einzelnen Funktion höherer Ordnung lösen.
Das Bewertungssystem basiert auf fünf Sternen und besteht einfach aus den Ganzzahlen 1:5.
Die emphatischen Kundinnen und Kunden vergeben nur Bewertungen von 1 oder 5, und wir wollen wissen, ob eine Kundin oder ein Kunde dieses Verhalten zeigt.
Implementiere all_15(), die einen Vektor von Bewertungen entgegennimmt und true zurückgibt, wenn alle Bewertungen entweder 1 oder 5 sind, und andernfalls false.
julia> ratings = [2, 3, 4, 4, 1];
julia> all_15(ratings)
false
julia> ratings = [1, 5, 5, 1, 5];
julia> all_15(ratings)
true
Wir müssen die besonders emphatischen Kundinnen und Kunden von den anderen trennen.
Implementiere emphatics(), die ein Wörterbuch aus Kundinnen und Kunden und ihren Bewertungen entgegennimmt.
Sie gibt ein entsprechendes Wörterbuch mit denen zurück, die ausschließlich Bewertungen mit 1 oder 5 Sternen vergeben.
julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);
julia> names = ("c1", "c2", "c3", "c4");
julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
"c2" => [1, 1, 5, 5, 1]
"c1" => [2, 3, 5, 1, 1]
"c3" => [4, 5, 5, 3, 2]
"c4" => [5, 5, 1, 1, 5]
julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
"c2" => [1, 1, 5, 5, 1]
"c4" => [5, 5, 1, 1, 5]
Da die emphatischen Kundinnen und Kunden nur die Bewertungen 1 und 5 verwenden, ist es rechenfreundlicher, diese in 0 und 1 umzuwandeln.
Implementiere tobinary(), die einen Vektor emphatischer Bewertungen entgegennimmt.
Sie gibt binäre Bewertungen zurück, wobei 1 in 0 und 5 in 1 umgewandelt wurde.
julia> ratings = [1, 1, 5, 5, 1];
julia> tobinary(ratings)
5-element Vector{Int64}:
0
0
1
1
0
Unsere Algorithmen verwenden Eingaben vom Typ Matrix, also müssen wir die Daten in eine solche umwandeln.
Implementiere tobinarymatrix(), die einen Vektor aus emphatischen Bewertungsvektoren entgegennimmt.
Sie gibt eine Matrix der transformierten Daten zurück, wobei jeder Bewertungsvektor eine Zeile der Matrix ist.
julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];
julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
0 0 1 1 0
1 1 0 0 1
Melde dich bei Exercism an, um Julia mit 35 Konzepte128 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.