Tracks
/
Julia
Julia
/
Übungen
/
Käseclub
Käseclub

Käseclub

Lernübung

Einführung

Technisch gesehen ist eine Higher Order Function einfach eine Funktion, die mindestens eines von beidem tut:

  • Sie nimmt eine Funktion als eines ihrer Argumente entgegen.
  • Sie gibt eine Funktion als ihr Ergebnis zurück.

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.

Mit Kollektionen arbeiten

An diesem Punkt im Lehrplan haben wir bereits verschiedene Wege gesehen, eine Operation auf alle Elemente einer iterierbaren Kollektion wie einem Vector anzuwenden:

  • Eine Schleife verwenden (wie die meisten Programmiersprachen seit den Anfängen des digitalen Rechnens).
  • Eine Comprehension verwenden (im Python-Stil).
  • Broadcasting verwenden (eine typisch Julia-Syntax, auch wenn sie R, Matlab und NumPy viel verdankt).

Dieses Konzept konzentriert sich auf Funktionen höherer Ordnung (bekannt aus jeder funktionalen Sprache wie Haskell oder F#).

Weitere mögliche Ansätze sind:

  • Rekursion (wie in Sprachen der ML-Familie).
    • Julia erlaubt das, aber ohne Tail-Call-Optimierung droht ein Stack Overflow.
  • Metaprogrammierung mit Makros (traditionell ein Merkmal von Lisp).
    • Das wird in der fortgeschrittenen Julia-Programmierung häufig verwendet, aber in den meisten Fällen gilt: mit Vorsicht anwenden.
    • Andere Optionen sind wahrscheinlich leichter zu schreiben und viel leichter zu debuggen.

Filtern

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.

Abbilden

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.

Reduzieren

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.

MapReduce

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.

Anleitung

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.

Note

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.

1. Kundinnen und Kunden klassifizieren

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

2. Emphatische Kundinnen und Kunden herausfiltern

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]

3. Bewertungen in Binärwerte umwandeln

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

4. Bewertungen in eine Matrix umwandeln

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
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Julia Exercism

Bereit, mit Käseclub zu starten?

Melde dich bei Exercism an, um Julia mit 35 Konzepte128 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.