Julia: benutzerdefinierte Typen

In diesem Artikel werden wir erwägen, dem Julia-Programm einen benutzerdefinierten Datentyp hinzuzufügen und Standardfunktionen zu überladen, um bequem mit dem neuen Typ arbeiten zu können.


Was ist ein benutzerdefinierter Typ?


Ein schreckliches Geheimnis - die Grenze zwischen den Datentypen "Benutzer" und "integriert" in Julia fehlt praktisch. In jedem Julia-Programm können Sie Ihre eigenen Datentypen definieren, und die Arbeit mit ihnen unterscheidet sich kaum von der Arbeit mit den integrierten Daten, weder hinsichtlich des Schreibens von Code noch hinsichtlich der Geschwindigkeit der Ausführung dieses Codes (Hallo, Python). Eingabedatentypen können entweder primitiv oder zusammengesetzt sein. Letztere werden ebenfalls in abstrakte und konkrete Typen unterteilt.


Weiter werden wir ein Beispiel für Verbundbetontypen betrachten, da Bestimmte Objekte im Speicher sind bestimmte Typen, und abstrakte Objekte werden nur zum Erstellen einer Hierarchie benötigt.


Zusammengesetzte Typen werden durch die Schlüsselwörter struct und mutable struct . Im ersten Fall wird ein unveränderlicher Typ erstellt:


 #    x  y struct Point x y end 

Objekte vom Typ Point unveränderlich, d.h. Der Feldwert in einem einmal erstellten Objekt kann nicht geändert werden.


 julia> p = Point(2, 3) Point(2, 3) julia> px 2 julia> py 3 julia> px = 4 ERROR: setfield! immutable struct of type Point cannot be changed 

Sie können jedoch einen veränderlichen Typ erstellen:


 mutable struct MutablePoint x y end julia> mp = MutablePoint(3, 4) MutablePoint(3, 4) julia> mp.y = 2 2 julia> mp MutablePoint(3, 2) 

Aus Effizienzgründen können den Strukturfeldern und der Struktur selbst Typanmerkungen hinzugefügt werden. Zum Beispiel:


 struct ParametricPoint{T<:Real} x::T y::T end 

Dies bedeutet, dass der Typ ParametricPoint durch den Typ T ParametricPoint und dass die Felder x und y nun Werte desselben Typs T Das Verhalten wird wie folgt sein:


 julia> ParametricPoint(1, 1) ParametricPoint{Int64}(1, 1) julia> ParametricPoint{Float64}(1, 1) ParametricPoint{Float64}(1.0, 1.0) julia> ParametricPoint{Rational}(1, 1) ParametricPoint{Rational}(1//1, 1//1) # :        T julia> ParametricPoint{Int}(1, 3.4) ERROR: InexactError: Int64(3.4) # :    julia> ParametricPoint(1, 1.0) ERROR: MethodError: no method matching Point(::Int64, ::Float64) # :  T      julia> ParametricPoint{Any}(1,1) ERROR: TypeError: in Point, in T, expected T<:Real, got Type{Any} 

Schauen wir uns als nächstes ein etwas komplexeres Beispiel eines Containertyps an, um zu sehen, welche Methoden in der Standardbibliothek für die Arbeit mit solchen Typen enthalten sind.


Zwei-Wege-Warteschlange


Wir werden aus Knoten vom Typ DequeNode eine DequeNode Warteschlange DequeNode , die einen Datensatz und Links zu den vorherigen und nächsten Elementen der Warteschlange enthält. Der Typ DequeNode muss veränderbar sein, weil Links müssen neu geschrieben werden.


 mutable struct DequeNode{T} data::T prev::DequeNode{T} next::DequeNode{T} #   -       function DequeNode{T}() where T node = new{T}() node.prev = node.next = node return node end #     function DequeNode{T}(x) where T node = new{T}() node.data = x node.prev = node.next = node return node end end 

In diesem Fall werden interne Konstruktoren benötigt, da Sie beim Erstellen eines Objekts standardmäßig die Werte aller Felder mit Argumenten übergeben müssen. Da der Typ selbst referenziert ist, kann ein Standardkonstruktor das allererste Objekt dieses Typs nicht erstellen. Daher erstellen wir spezielle Konstruktoren, die einen Warteschlangenknoten erstellen, der auf sich selbst verweist.


Die Warteschlange selbst kann als fiktiver "Eingabeknoten" dargestellt werden, der keine Daten enthält. Der next Link verweist auf das erste Element der Warteschlange und auf das letzte. Bei einer leeren Warteschlange verweisen beide Links auf den Eingabeknoten. Eine solche Organisation vereinfacht die Verfahren zum Hinzufügen und Entfernen von Knoten.


 struct Deque{T} entry::DequeNode{T} end Deque{T}() where T = Deque{T}(DequeNode{T}()) Deque() = Deque{Any}() 

Für die Struktur der Warteschlange selbst ist kein interner Konstruktor mehr erforderlich.


Elemente hinzufügen


Die Standardprozeduren für eine Warteschlange bestehen darin, am Anfang und am Ende ein Element hinzuzufügen und Elemente am Anfang und am Ende zu extrahieren. Für diese Prozeduren verfügt die Standardbibliothek bereits über Funktionen, zu denen Sie nur Methoden hinzufügen müssen, um mit einem anderen Typ zu arbeiten:


  • push!(collection, element) - füge der Sammlung ein Element hinzu (für eine bestellte Sammlung - am Ende)
  • pushfirst!(collection, element) - pushfirst!(collection, element) am Anfang einer geordneten Sammlung ein Element hinzu
  • pop!(collection) - löscht einen Artikel aus der Sammlung (für eine bestellte Sammlung - vom Ende)
  • popfirst!(collection) - Entfernen Sie den ersten Artikel aus einer bestellten Sammlung

Alle diese Funktionen befinden sich im Basismodul, und Sie müssen neue Methoden hinzufügen. Zusätzlich schreiben wir eine Funktion, um zu überprüfen, ob die Sammlung nicht leer ist - Base.isempty(collection) .


 Base.isempty(deq::Deque) = deq.entry.prev ≡ deq.entry function Base.push!(deq::Deque{T}, elt) where T tail = deq.entry.prev new_item = DequeNode{T}(elt) new_item.prev, new_item.next = tail, deq.entry tail.next = deq.entry.prev = new_item return deq end function Base.pushfirst!(deq::Deque{T}, elt) where T head = deq.entry.next new_item = DequeNode{T}(elt) new_item.prev, new_item.next = deq.entry, head head.prev = deq.entry.next = new_item return deq end function Base.pop!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) last = deq.entry.prev last.prev.next = deq.entry deq.entry.prev = last.prev return last.data end function Base.popfirst!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) first = deq.entry.next first.next.prev = deq.entry deq.entry.next = first.next return first.data end 

Die Funktion push!() Macht es fast trivial, einen Warteschlangenkonstruktor basierend auf einer beliebigen Sammlung zu schreiben:


 function Deque(itr) d = Deque{eltype(itr)}() for elt in itr push!(d, elt) end return d end #        Deque(args...) = Deque(args) 

Jetzt kann der neue Typ fast als Warteschlange verwendet werden, wenn wir nur Operationen hinzufügen und entfernen müssen und keine Funktion wie peek() benötigen (sehen Sie sich das Element im Kopf oder Ende der Warteschlange an, ohne es zu entfernen). Für den zerstörungsfreien Zugriff auf Elemente ist es zweckmäßig, die Iterationsfunktion über den Container zu definieren.


Iteration


Julia unterstützt das Konzept von Iteratoren zum Durchlaufen von Containerelementen. Für die Iteration müssen Sie eine einzelne Funktion definieren - iterate(container, state) , wobei state den aktuellen Status des Iterators bestimmt. Insbesondere ist das Konstrukt for x in collection syntaktischer Zucker für so etwas:


 let nextstate = iterate(collection) while nextstate ≢ nothing (x, state) = nextstate <- > nextstate = iterate(collection, state) end end 

Die Funktion iterate() verwendet einen Container und einen „Iteratorstatus“ als Argument und gibt entweder ein Tupel aus dem Containerelement und dem nächsten Iteratorstatus zurück oder nothing wenn der Container beendet wurde.
Für eine Warteschlange ist es logisch, den Warteschlangenknoten, den die Iteration erreicht hat, als "Status" zu verwenden:


 function Base.iterate(deq::Deque{T}, state::DequeNode{T}=deq.entry) where T nextstate = state.next nextstate ≡ deq.entry ? nothing : (nextstate.data, nextstate) end 

Jetzt können die Elemente der Warteschlange mit einer for Schleife sortiert werden:


 julia> for x in Deque(1:10) println(x); end 1 2 3 4 5 6 7 8 9 10 

Ein großes Plus ist, dass viele der Funktionen der Standard-Julia-Bibliothek über verallgemeinerte Schnittstellen geschrieben werden, insbesondere über Iteration. Somit wird automatisch ermittelt, ob ein Element automatisch zu einem Container gehört:


 julia> 5 ∈ Deque(3:10) true julia> 100 ∈ Deque(-5:5) false 

Wir finden auch, dass die first() -Methode, die das erste Element der Sammlung zurückgibt, zu funktionieren begann. Um das Bild zu vervollständigen, ergänzen wir die last() -Methode, um das letzte Element zu erhalten und in umgekehrter Reihenfolge zu iterieren:


 function Base.iterate(r::Base.Iterators.Reverse{Deque{T}}, state::DequeNode{T}=r.itr.entry) where T nextstate = state.prev nextstate ≡ r.itr.entry ? nothing : (nextstate.data, nextstate) end function Base.last(deq::Deque) isempty(deq) && throw(ArgumentError("deque must be non-empty")) deq.entry.prev.data end julia> for x in Iterators.reverse(Deque(1:10)) println(x); end 10 9 8 7 6 5 4 3 2 1 

Die Warteschlangenschnittstelle als Datenstruktur ist nun vollständig definiert


BedienungFunktionsname
am Ende einfügenpush!(deque, element)
Einfügen an den Anfangpushfirst!(deque, element)
Entfernung von Anfang anpopfirst!(deque) (gibt das gelöschte Element zurück)
Entfernung vom Endepop!(deque) (gibt das gelöschte Element zurück)
Zeigen Sie das erste Element anfirst(deque)
letzten Artikel anzeigenlast(deque)

Druckstruktur


Obwohl die Warteschlange vollständig als Datenstruktur fungiert, stellt sie beim Drucken immer noch ein formloses Durcheinander dar:


 julia> Deque() Deque{Any}(DequeNode{Any}(#undef, DequeNode{Any}(#= circular reference @-1 =#), DequeNode{Any}(#= circular reference @-1 =#))) 

Fügen Sie eine Methode hinzu, um die Warteschlange in einer menschlicheren Form zu drucken. Die Ausgabe der Struktur an das Terminal wird von der Funktion Base.show() gesteuert, die wir überladen werden:


 function Base.show(io::IO, deq::Deque{T}) where T print(io, "Deque{", T, "}(") next = iterate(deq) if next ≢ nothing item, state = next show(io, item) next = iterate(deq, state) while next ≢ nothing item, state = next print(io, ", ") show(io, item) next = iterate(deq, state) end end print(io, ")") end #        julia> Deque(1:5) Deque{Int64}(1, 2, 3, 4, 5) #       julia> Deque(Deque(), Deque(), Deque()) Deque{Deque{Any}}(Deque{Any}(), Deque{Any}(), Deque{Any}()) 

Zugriff und Änderung nach Index


Grundsätzlich kann eine Warteschlange auch als Container mit Zugriff per Index verwendet werden. Das Abrufen und Ändern eines Werts nach Index wird durch die setindex!() getindex() und setindex!() . Wir implementieren die notwendigen Methoden für die Warteschlange.


 function Base.getindex(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else return next[1] end end function Base.setindex!(deq::Deque, val, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.data = val return val end end 

Es ist auch nützlich, die Funktion zum Einfügen eines Elements an einer beliebigen Stelle insert!() Und ein Element von einer beliebigen Position zu entfernen:


 function Base.insert!(deq::Deque{T}, i::Integer, val) where T i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing push!(deq, val) else record = next[2] new_node = DequeNode{T}(val) new_node.prev, new_node.next = record.prev, record record.prev = record.prev.next = new_node deq end end function Base.deleteat!(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.prev.next, record.next.prev = record.next, record.prev end end 

Andere


Wie bereits erwähnt, werden eine Reihe von Funktionen der Standardbibliothek mithilfe von Iteratoren geschrieben, sodass sie automatisch mit jedem Typ arbeiten können, für den die Funktion iterate() definiert ist. Insbesondere funktionieren Funktionen wie map() , collect() und Reduktionsmethoden wie sum() oder minimum() .
Eine Funktion zum Ermitteln der Länge einer Warteschlange kann beispielsweise folgendermaßen geschrieben werden:


 Base.length(deq::Deque) = sum(x->1, deq) julia> length(Deque(1, 1, 2, 3, 5, 8, 13)) 7 

Fazit


Wie Sie sehen, ist es sehr einfach, in Julia einen eigenen Datentyp zu erstellen und damit bequem zu arbeiten. Aufgrund der Verwendung der verallgemeinerten Programmierung in der Standardbibliothek reicht es normalerweise aus, mehrere Grundfunktionen zu definieren, und einige abhängige Funktionen funktionieren automatisch. Wenn Sie also push!(container, element) , um ein einzelnes Element hinzuzufügen, können Sie feststellen, dass push!(container, elements...) eine beliebige Anzahl von Argumenten hinzufügt. Nachdem wir einen Iterator definiert haben, erhalten wir im Allgemeinen automatisch alle Funktionen der Standardbibliothek für die Arbeit mit einem abstrakten Container.


Viel Spaß beim Hacken!

Source: https://habr.com/ru/post/de463857/


All Articles