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
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.
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.
+ ist derzeit eine Funktion mit 161 Methoden).... kann das allerdings teilweise umgehen).dispatched und gibt ein Ergebnis zurück.MethodError ausgelöst.Beachte, dass Julia dynamisch typisiert ist, der Methoden-Dispatch findet also zur Laufzeit statt.
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
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)
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!
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.
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.
Implementiere eine Funktion name(), die den Namen eines Haustiers zurückgibt.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Implementiere meets()-Methoden für die folgenden Begegnungen:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
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."
Was passiert, wenn sie auf ihrem Spaziergang auf ein anderes Haustier treffen, zum Beispiel auf ein Pferd?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
Es gibt viele andere Dinge, auf die Haustiere treffen können und die keine Haustiere sind: Autos, Menschen, Pflanzen, Naturkatastrophen, Asteroiden… Was passiert dann?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
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.
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.
Melde dich bei Exercism an, um Julia mit 35 Konzepte128 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.