9Ein Fallbeispiel: Der parametrisierte Datentyp PComplex
Wir wollen als neuen numerischen Typen komplexe Zahlen in Polardarstellung \(z=r e^{i\phi}=(r,ϕ)\) einführen.
Der Typ soll sich in die Typhierarchie einfügen als Subtyp von ‘Number’.
\(r\) und \(\phi\) sollen Gleitkommazahlen sein. (Im Unterschied zu komplexen Zahlen in ‘kartesischen’ Koordinaten hat eine Einschränkung auf ganzzahlige Werte von r oder ϕ mathematisch wenig Sinn.)
9.1 Die Definition von PComplex
Ein erster Versuch könnte so aussehen:
struct PComplex1{T <: AbstractFloat} <: Number r :: T ϕ :: Tendz1 =PComplex1(-32.0, 33.0)z2 =PComplex1{Float32}(12, 13)@show z1 z2;
Es ist nicht möglich, in einer Julia-Session eine einmal definierte struct später umzudefinieren. Daher verwende ich verschiedene Namen. Eine andere Möglichkeit ist z.B. die Verwendung von ProtoStructs.jl.
Julia stellt automatisch default constructors zur Verfügung:
den Konstruktor PComplex1, bei dem der Typ T von den übergebenen Argumenten abgeleitet wird und
Konstruktoren PComplex{Float64},... mit expliziter Typangabe. Hier wird versucht, die Argumente in den angeforderten Typ zu konvertieren.
Wir wollen nun, dass der Konstruktor noch mehr tut. In der Polardarstellung soll \(0\le r\) und \(0\le \phi<2\pi\) gelten.
Wenn die übergebenen Argumente das nicht erfüllen, sollten sie entsprechend umgerechnet werden.
Dazu definieren wir einen inner constructor, der den default constructor ersetzt.
Ein inner constructor ist eine Funktion innerhalb der struct-Definition.
In einem inner constructor kann man die spezielle Funktion new verwenden, die wie der default constructor wirkt.
struct PComplex{T <: AbstractFloat} <: Number r :: T ϕ :: TfunctionPComplex{T}(r::T, ϕ::T) where T<:AbstractFloatif r<0# flip the sign of r and correct phi r =-r ϕ +=πendif r==0 ϕ=0end# normalize r=0 case to phi=0 ϕ =mod(ϕ, 2π) # map phi into interval [0,2pi)new(r, ϕ) # new() ist special function,end# available only inside inner constructorsend
z1 =PComplex{Float64}(-3.3, 7π+1)
PComplex{Float64}(3.3, 1.0)
Für die explizite Angabe eines inner constructors müssen wir allerdings einen Preis zahlen: Die sonst von Julia bereitgestellten default constructors fehlen.
Den Konstruktor, der ohne explizite Typangabe in geschweiften Klammern auskommt und den Typ der Argumente übernimmt, wollen wir gerne auch haben:
PComplex(r::T, ϕ::T) where {T<:AbstractFloat} =PComplex{T}(r,ϕ)z2 =PComplex(2.0, 0.3)
PComplex{Float64}(2.0, 0.3)
9.2 Eine neue Schreibweise
Julia verwendet // als Infix-Konstruktor für den Typ Rational. Sowas Schickes wollen wir auch.
In der Elektronik/Elektrotechnik werden Wechselstromgrößen durch komplexe Zahlen beschrieben.. Dabei ist eine Darstellung komplexer Zahlen durch “Betrag” und “Phase” üblich und sie wird gerne in der sogenannten Versor-Form (engl. phasor) dargestellt: \[
z= r\enclose{phasorangle}{\phi} = 3.4\;\enclose{phasorangle}{45^\circ}
\] wobei man in der Regel den Winkel in Grad notiert.
Mögliche Infix-Operatoren in Julia
In Julia ist eine große Anzahl von Unicode-Zeichen reserviert für die Verwendung als Operatoren. Die definitive Liste ist im Quellcode des Parsers.
Auf Details werden wir in einem späteren Kapitel noch eingehen.
Das Winkel-Zeichen ∠ steht leider nicht als Operatorsymbol zur Verfügung. Wir weichen aus auf ⋖. Das kann in Julia als als \lessdot<tab> eingegeben werden.
(Die Typ-Annotation – Real statt AbstractFloat – ist ein Vorgriff auf kommende weitere Konstruktoren. Im Moment funktioniert der Operator ⋖ erstmal nur mit Floats.)
Natürlich wollen wir auch die Ausgabe so schön haben. Details dazu findet man in der Dokumentation.
usingPrintffunctionBase.show(io::IO, z::PComplex)# wir drucken die Phase in Grad, auf Zehntelgrad gerundet, p = z.ϕ *180/π sp =@sprintf"%.1f" pprint(io, z.r, "⋖", sp, '°')end@show z3;
z3 = 2.0⋖90.0°
9.3 Methoden für PComplex
Damit unser Typ ein anständiges Mitglied der von Number abstammenden Typfamilie wird, brauchen wir allerdings noch eine ganze Menge mehr. Es müssen Arithmetik, Vergleichsoperatoren, Konvertierungen usw. definiert werden.
Wir beschränken uns auf Multiplikation und Quadratwurzeln.
Module
Um die methods der existierenden Funktionen und Operationen zu ergänzen, muss man diese mit ihrem ‘vollen Namen’ ansprechen.
Alle Objekte gehören zu einem Namensraum oder module.
Die meisten Basisfunktionen gehören zum Modul Base, welches standardmäßig immer ohne explizites using ... geladen wird.
Solange man keine eigenen Module definiert, sind die eigenen Definitionen im Modul Main.
Das Macro @which, angewendet auf einen Namen, zeigt an, in welchem Modul der Name definiert wurde.
f(x) =3x^3@which f
Main.Notebook
wp =@which+ws =@which(sqrt)println("Modul für Addition: $wp, Modul für sqrt: $ws")
(Da das Operatorsymbol kein normaler Name ist, muss der Doppelpunkt bei der Zusammensetzung mit Base. sein.)
Wir können allerdings noch nicht mit anderen numerischen Typen multiplizieren. Dazu könnte man nun eine Vielzahl von entsprechenden Methoden definieren. Julia stellt für numerische Typen noch einen weiteren Mechanismus zur Verfügung, der das etwas vereinfacht.
9.4 Typ-Promotion und Konversion
In Julia kann man bekanntlich die verschiedensten numerischen Typen nebeneinander verwenden.
1//3+5+5.2+0xff
265.53333333333336
Wenn man in die zahlreichen Methoden schaut, die z.B. für + und * definiert sind, findet man u.a. eine Art ‘catch-all-Definition’
Assigning to an array converts to the array’s element type.
Assigning to a field of an object converts to the declared type of the field.
Constructing an object with new converts to the object’s declared field types.
Assigning to a variable with a declared type (e.g. local x::T) converts to that type.
A function with a declared return type converts its return value to that type.
– und natürlich in promote()
Für selbstdefinierte Datentypen kann man convert() um weitere Methoden ergänzen.
Für Datentypen innerhalb der Number-Hierarchie gibt es wieder eine ‘catch-all-Definition’
convert(::Type{T}, x::Number) where {T<:Number} =T(x)
Also: Wenn für einen Typen T aus der Hierarchie T<:Number ein Konstruktor T(x) mit einem numerischen Argument x existiert, dann wird dieser Konstruktor T(x) automatisch für Konvertierungen benutzt. (Natürlich können auch speziellere Methoden für convert() definiert werden, die dann Vorrang haben.)
9.4.4 Weitere Konstruktoren für PComplex
## (a) r, ϕ beliebige Reals, z.B. Integers, RationalsPComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =PComplex{T}(convert(T, r), convert(T, ϕ))PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =PComplex{promote_type(Float64, T1, T2)}(r, ϕ) ## (b) Zur Umwandlung von Reals: Konstruktor mit ## nur einem Argument rPComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =PComplex{T}(convert(T, r), convert(T, 0)) PComplex(r::S) where {S<:Real} =PComplex{promote_type(Float64, S)}(r, 0.0)## (c) Umwandlung Complex -> PComplexPComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =PComplex{T}(abs(z), angle(z))PComplex(z::Complex{S}) where {S<:Real} =PComplex{promote_type(Float64, S)}(abs(z), angle(z))
Wir brauchen nun noch promotion rules, die festlegen, welcher Typ bei promote(x::T1, y::T2) herauskommen soll. Damit wird promote_type() intern um die nötigen weiteren Methoden erweitert.
9.4.5Promotion rules für PComplex
Base.promote_rule(::Type{PComplex{T}}, ::Type{S}) where {T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where {T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
Regel:
Wenn ein PComplex{T} und ein S<:Real zusammentreffen, dann sollen beide zu PComplex{U} umgewandelt werden, wobei U der Typ ist, zu dem S und T beide umgewandelt (promoted) werden können.
Regel
Wenn ein PComplex{T} und ein Complex{S} zusammentreffen, dann sollen beide zu PComplex{U} umgewandelt werden, wobei U der Typ ist, zu dem S und T beide umgewandelt werden können.
Damit klappt nun die Multiplikation mit beliebigen numerischen Typen.
struct PComplex{T <: AbstractFloat} <: Number r :: T ϕ :: TfunctionPComplex{T}(r::T, ϕ::T) where T<:AbstractFloatif r<0# flip the sign of r and correct phi r =-r ϕ +=πendif r==0 ϕ=0end# normalize r=0 case to phi=0 ϕ =mod(ϕ, 2π) # map phi into interval [0,2pi)new(r, ϕ) # new() ist special function,end# available only inside inner constructorsend# additional constructorsPComplex(r::T, ϕ::T) where {T<:AbstractFloat} =PComplex{T}(r,ϕ)PComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =PComplex{T}(convert(T, r), convert(T, ϕ))PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =PComplex{promote_type(Float64, T1, T2)}(r, ϕ) PComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =PComplex{T}(convert(T, r), convert(T, 0)) PComplex(r::S) where {S<:Real} =PComplex{promote_type(Float64, S)}(r, 0.0)PComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =PComplex{T}(abs(z), angle(z))PComplex(z::Complex{S}) where {S<:Real} =PComplex{promote_type(Float64, S)}(abs(z), angle(z))# nice input⋖(r::Real, ϕ::Real) =PComplex(r, π*ϕ/180)# nice outputusingPrintffunctionBase.show(io::IO, z::PComplex)# wir drucken die Phase in Grad, auf Zehntelgrad gerundet, p = z.ϕ *180/π sp =@sprintf"%.1f" pprint(io, z.r, "⋖", sp, '°')end# arithmeticBase.sqrt(z::PComplex) =PComplex(sqrt(z.r), z.ϕ /2)Base.:*(x::PComplex, y::PComplex) =PComplex(x.r * y.r, x.ϕ + y.ϕ)# promotion rulesBase.promote_rule(::Type{PComplex{T}}, ::Type{S}) where {T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where {T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}