Tracks
/
Julia
Julia
/
Übungen
/
Begegnungen
Begegnungen

Begegnungen

Lernübung

Einführung

An diesem Punkt im Lehrplan hast du schon viele Funktionsdefinitionen gesehen. Meist wurden sie vorgestellt, ohne viel zur Ausgabe in der REPL zu sagen:

julia> f(x) = x^2
f (generic function with 1 method)

Warum steht da „generic function with 1 method“?

Angenommen, wir wollen die Argumenttypen genauer angeben.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

Oder wir ändern die Anzahl der Argumente:

julia> f(x, y) = x * y
f (generic function with 4 methods)

Es scheint, dass neue Definitionen derselben Funktion „Methoden“ heißen. Methoden werden zu einer Liste hinzugefügt, anstatt die alte Definition zu überschreiben.

Wir können die Methoden leicht auflisten:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

Wenn wir die Funktion f aufrufen, weiß Julia irgendwie, welche Methode sie verwenden soll:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

Was ist eine Funktion?

Laut Handbuch ist eine Funktion ein Objekt, das ein Tupel von Argumenten auf einen Rückgabewert abbildet.

Normalerweise denken wir bei den Funktionsargumenten in f(2, 3) an zwei getrennte Dinge, aber der Compiler sieht sie als ein einziges Tupel. Das gilt sogar für ein einzelnes Argument (allerdings müssen wir ein Komma hinzufügen, um das zu verdeutlichen):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

Betrachten wir die Funktion als Blackbox, können wir sagen, dass ein Tupel hineingeht und ein Rückgabewert herauskommt. Formaler ausgedrückt wird das Tupel auf einen Rückgabewert abgebildet.

Wie gehen Funktionen mit unterschiedlicher Anzahl und unterschiedlichen Typen von Argumenten um?

Man bezeichnet das üblicherweise als „Polymorphismus“.

Das ist seit mehreren Jahrzehnten ein großes Thema in der Informatik, und es wurden verschiedene Ansätze gewählt.

Julias Ansatz ist ungewöhnlich, aber nicht einzigartig.

  • Funktionen sind „generisch“, das heißt, es können mehrere Methoden an sie angehängt sein.
  • Funktionen sind nur Namen, bis Methoden an sie angehängt sind.
  • Methoden können nach Bedarf hinzugefügt werden, potenziell in großer Zahl (der Operator + ist derzeit eine Funktion mit 161 Methoden).
  • Wenn eine Funktion aufgerufen wird, untersucht Julia die Aufrufsignatur: die Anzahl der Argumente und den aktuellen Typ von jedem.
  • Die Methode, die am besten zur Aufrufsignatur passt, wird ausgewählt.
    • Im Allgemeinen muss die Anzahl der Argumente genau übereinstimmen (der Splat/Slurp-Operator ... kann das allerdings teilweise umgehen).
    • Methoden, deren Typen genau zur Aufrufsignatur passen, werden bevorzugt.
    • Falls nötig, schaut Julia sich die Supertypen jedes Arguments in der Aufrufsignatur an, um die am besten passende, spezifischste Methode zu finden.
  • Die gewählte Methode wird mit den gegebenen Argumenten dispatched und gibt ein Ergebnis zurück.
  • Wenn keine passende Methode gefunden wird, wird ein MethodError ausgelöst.

Beachte, dass Julia dynamisch typisiert ist, der Methoden-Dispatch findet also zur Laufzeit statt.

Multiple Dispatch: ein Beispiel

Der vorherige Abschnitt hatte viele Worte, also übersetzen wir diese Ideen in Code. Der Einfachheit halber haben die folgenden Funktionen nur ein Argument, aber es ist nicht ungewöhnlich, Funktionen mit Dutzenden von Argumenten zu finden (etwa in Plot-Paketen oder im maschinellen Lernen).

Definiere zunächst einige eigene Typen, um geometrische Formen darzustellen.

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

Als Nächstes können wir Methoden definieren, um die Fläche jeder Form zu berechnen. Jede nimmt ein Argument, und zwar jeweils von einem anderen konkreten, selbst definierten Typ.

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

Jetzt können wir einige Formen erstellen und die Flächen berechnen, wobei wir uns auf Julias Dispatch-Mechanismus verlassen, der für jede die richtige Formel verwendet.

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

Parametrische Methoden

Methodensignaturen können Dummy-Typwerte annehmen, in etwa so wie parametrische Typen.

Die Syntax ist eigenwillig und erfordert eine where-Klausel:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

Das obige Beispiel verlangt, dass beide Argumente denselben Typ T verwenden, egal welcher das ist.

Wir können genauer werden, indem wir eine Einschränkung für T angeben:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

Mit dieser Methode geben nur identische numerische Typen true zurück.

Mehrere Dummy-Variablen sind möglich, durch Kommas getrennt in der where-Klausel.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

Fazit

Multiple Dispatch ist in Julia sehr flexibel und äußerst leistungsfähig, daher wird es sehr breit eingesetzt.

Wie das Beispiel der 161 Methoden für + nahelegt, zielen Julia-Programmierer darauf ab, Methoden mit eng festgelegten Typen zu definieren. Zwei Ganzzahlen zu addieren ist auf Bytecode-Ebene etwas ganz anderes als zwei Gleitkommazahlen zu addieren, und eine Ganzzahl zu einer Gleitkommazahl zu addieren bringt zusätzlich die Komplikation der Typ-Promotion mit sich.

Je spezifischer der Typ, desto mehr Optimierung kann der Compiler durchführen: bei einer kleinen Übung in Exercism von geringer Bedeutung, aber entscheidend für die Performance in den großen numerischen Simulationen, für die Julia gedacht ist.

Wenn du weißt, welche Eingabewerte eine Methode erhalten wird, sag es dem Compiler!

Anleitung

In dieser Übung implementierst du eine Simulation von Begegnungen. Damit lernst du die Grundlagen von Multiple Dispatch kennen, dem wichtigsten Paradigma von Julia.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

Im Allgemeinen geht es bei Begegnungen darum, dass eine Entität a auf eine andere Entität b trifft und darauf reagiert.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

Zunächst simulieren wir, was passiert, wenn Katzen und Hunde aufeinandertreffen.

1. Definiere eigene Typen

Füge zuerst einen abstrakten Typ Pet hinzu.

Definiere dann die Typen Cat und Dog als Untertypen von Pet. Jeder hat ein einzelnes Feld name.

2. Ermittle den Namen des Haustiers

Implementiere eine Funktion name(), die den Namen eines Haustiers zurückgibt.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. Definiere, was passiert, wenn Katzen und Hunde aufeinandertreffen

Implementiere meets()-Methoden für die folgenden Begegnungen:

  • Wenn zwei Hunde aufeinandertreffen, ist das Ergebnis sniff.
  • Wenn eine Katze auf einen Hund trifft, ist das Ergebnis hisses.
  • Wenn ein Hund auf eine Katze trifft, ist das Ergebnis chases.
  • Wenn zwei Katzen aufeinandertreffen, ist das Ergebnis slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Definiere eine Begegnung zwischen zwei Entitäten

Implementiere die Funktion encounter() mit einer Methode, die berichtet, was passiert, wenn zwei benannte Entitäten aufeinandertreffen.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. Definiere einen Fallback für Begegnungen zwischen Haustieren

Was passiert, wenn sie auf ihrem Spaziergang auf ein anderes Haustier treffen, zum Beispiel auf ein Pferd?

  • Wenn ein Haustier auf ein anderes Haustier trifft, das es nicht erkennt, ist das Ergebnis is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. Definiere einen Fallback, falls ein Haustier auf etwas trifft, das es nicht kennt

Es gibt viele andere Dinge, auf die Haustiere treffen können und die keine Haustiere sind: Autos, Menschen, Pflanzen, Naturkatastrophen, Asteroiden… Was passiert dann?

  • Wenn ein Haustier auf etwas trifft, das es noch nie gesehen hat, ist das Ergebnis runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. Definiere einen generischen Fallback

In unserer Simulation könnte es noch viele weitere Begegnungen geben. Ein Auto trifft auf einen Hund, zwei Elektronen begegnen sich und wechselwirken…

Es ist unmöglich, alle Begegnungen im Voraus abzudecken, deshalb implementieren wir einen generischen Fallback.

  • Wenn zwei unbekannte Dinge oder Wesen aufeinandertreffen, ist das Ergebnis nothing happens.
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Julia Exercism

Bereit, mit Begegnungen zu starten?

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