Typdeklaration in Julia erforderlich

16

Gibt es eine Möglichkeit, in Julia explizit zu verlangen (z. B. innerhalb eines Moduls oder Pakets), dass Typen deklariert werden müssen ? Hat zB oder Unterstützung für solche Überprüfungen? Bietet die Julia-Standarddistribution selbst einen statischen Code-Analysator oder ein gleichwertiges Gerät , mit dessen Hilfe diese Anforderung überprüft werden kann?PackageCompilerLint.jl

Nehmen wir als motivierendes Beispiel an, wir möchten sicherstellen, dass unsere wachsende Produktionscodebasis nur Code akzeptiert, der immer vom Typ deklariert ist , unter der Hypothese, dass große Codebasen mit Typdeklarationen tendenziell wartbarer sind.

Wenn wir diese Bedingung durchsetzen wollen, bietet Julia in ihrer Standardverteilung Mechanismen, um eine Typdeklaration zu erfordern, oder hilft sie, dieses Ziel zu erreichen? (zB irgendetwas, das über Linters, Commit-Hooks oder Ähnliches überprüft werden könnte?)

Amelio Vazquez-Reina
quelle
1
Ich bin mir nicht sicher, wie viel dies hilft, werde aber, ähnlich wie Bogumils Gedanken, hasmethod(f, (Any,) )zurückkehren, falsewenn kein Generikum definiert wurde. Sie müssten jedoch immer noch die Anzahl der Argumente anpassen (dh hasmethod(f, (Any,Any) )für eine Funktion mit zwei Argumenten).
Tasos Papastylianou

Antworten:

9

Die kurze Antwort lautet: Nein, derzeit gibt es kein Tool zur Typprüfung Ihres Julia-Codes. Dies ist jedoch im Prinzip möglich, und in der Vergangenheit wurden einige Arbeiten in dieser Richtung durchgeführt, aber es gibt derzeit keinen guten Weg, dies zu tun.

Die längere Antwort lautet, dass "Typanmerkungen" hier ein roter Hering sind. Was Sie wirklich wollen, ist die Typprüfung, sodass der breitere Teil Ihrer Frage tatsächlich die richtige Frage ist. Ich kann ein wenig darüber sprechen, warum Typanmerkungen ein roter Hering sind, einige andere Dinge, die nicht die richtige Lösung sind, und wie die richtige Art von Lösung aussehen würde.

Das Erfordernis von Typanmerkungen erreicht wahrscheinlich nicht das, was Sie wollen: Man könnte einfach ein ::Anybeliebiges Feld, Argument oder einen beliebigen Ausdruck einfügen und es hätte eine Typanmerkung, aber keine, die Ihnen oder dem Compiler etwas Nützliches über den tatsächlichen Typ dieser Sache sagt. Es fügt viel visuelles Rauschen hinzu, ohne tatsächlich Informationen hinzuzufügen.

Was ist mit konkreten Anmerkungen? Das ::Anyschließt aus, einfach alles anzuziehen (was Julia sowieso implizit tut). Es gibt jedoch viele vollkommen gültige Verwendungen von abstrakten Typen, die dies illegal machen würde. Zum Beispiel ist die Definition der identityFunktion

identity(x) = x

Welche konkrete Typanmerkung würden Sie xunter dieser Anforderung anbringen? Die Definition gilt für jeden x, unabhängig vom Typ - das ist der Punkt der Funktion. Die einzige korrekte Typanmerkung ist x::Any. Dies ist keine Anomalie: Es gibt viele Funktionsdefinitionen, die abstrakte Typen erfordern, um korrekt zu sein. Daher wäre es ziemlich einschränkend, diese zu zwingen, konkrete Typen zu verwenden, was für einen Julia-Code man schreiben kann.

Es gibt einen Begriff von "Typstabilität", über den in Julia oft gesprochen wird. Der Begriff scheint aus der Julia-Community zu stammen, wurde aber von anderen dynamischen Sprachgemeinschaften wie R aufgegriffen. Die Definition ist etwas schwierig, bedeutet aber in etwa, dass Sie, wenn Sie die konkreten Arten der Argumente einer Methode kennen, Sie kennen auch die Art des Rückgabewerts. Selbst wenn eine Methode typstabil ist, reicht dies nicht aus, um eine Typprüfung zu gewährleisten, da die Typstabilität keine Regeln für die Entscheidung enthält, ob eine Typprüfung durchgeführt wird oder nicht. Dies geht jedoch in die richtige Richtung: Sie möchten überprüfen können, ob jede Methodendefinition typstabil ist.

Sie viele möchten keine Typstabilität benötigen, selbst wenn Sie könnten. Seit Julia 1.0 ist es üblich geworden, kleine Gewerkschaften einzusetzen. Dies begann mit der Neugestaltung des Iterationsprotokolls, das nun nothinganzeigt, dass die Iteration durchgeführt wird, anstatt ein (value, state)Tupel zurückzugeben, wenn mehr Werte iteriert werden müssen. Die find*Funktionen in der Standardbibliothek verwenden auch einen Rückgabewert von, nothingum anzuzeigen, dass kein Wert gefunden wurde. Dies sind technisch gesehen Instabilitäten, aber sie sind beabsichtigt und der Compiler kann recht gut darüber nachdenken, wie sie die Instabilität optimieren. Zumindest kleine Gewerkschaften müssen also wahrscheinlich im Code zugelassen sein. Außerdem gibt es keinen klaren Ort, um die Grenze zu ziehen. Obwohl man vielleicht sagen könnte, dass ein Rückgabetyp vonUnion{Nothing, T} ist akzeptabel, aber nichts Unvorhersehbareres.

Was Sie jedoch wahrscheinlich wirklich wollen, anstatt Typanmerkungen oder Typstabilität zu benötigen, ist ein Tool, das überprüft, ob Ihr Code keine Methodenfehler auslösen kann, oder allgemeiner gesagt, dass er keine unerwarteten Fehler auslöst. Der Compiler kann häufig genau bestimmen, welche Methode an jedem Aufrufstandort aufgerufen wird, oder sie zumindest auf einige Methoden eingrenzen. Auf diese Weise wird schneller Code generiert - der vollständige dynamische Versand ist sehr langsam (viel langsamer als beispielsweise vtables in C ++). Wenn Sie andererseits falschen Code geschrieben haben, gibt der Compiler möglicherweise einen bedingungslosen Fehler aus: Der Compiler weiß, dass Sie einen Fehler gemacht haben, teilt Ihnen dies jedoch erst zur Laufzeit mit, da dies die Sprachsemantik ist. Es könnte erforderlich sein, dass der Compiler bestimmen kann, welche Methoden an jedem Aufrufstandort aufgerufen werden können: Das würde garantieren, dass der Code schnell ist und keine Methodenfehler vorliegen. Das sollte ein gutes Tool zur Typprüfung für Julia tun. Es gibt eine gute Grundlage für diese Art von Dingen, da der Compiler bereits einen Großteil dieser Arbeit im Rahmen der Codegenerierung erledigt.

StefanKarpinski
quelle
12

Dies ist eine interessante Frage. Die Schlüsselfrage ist, was wir als deklarierten Typ definieren . Wenn Sie meinen, dass ::SomeTypejede Methodendefinition eine Anweisung enthält, ist dies etwas schwierig, da Sie in Julia verschiedene Möglichkeiten zur dynamischen Codegenerierung haben. Vielleicht gibt es eine vollständige Lösung in diesem Sinne, aber ich weiß es nicht (ich würde es gerne lernen).

Mir fällt jedoch ein, dass es relativ einfacher zu sein scheint, zu überprüfen, ob eine in einem Modul definierte Methode Anyals Argument akzeptiert wird . Dies ist ähnlich, aber nicht äquivalent zu der früheren Aussage als:

julia> z1(x::Any) = 1
z1 (generic function with 1 method)

julia> z2(x) = 1
z2 (generic function with 1 method)

julia> methods(z1)
# 1 method for generic function "z1":
[1] z1(x) in Main at REPL[1]:1

julia> methods(z2)
# 1 method for generic function "z2":
[1] z2(x) in Main at REPL[2]:1

sehen für die methodsFunktion gleich aus , da die Signatur beider Funktionen xals akzeptiert Any.

Um nun zu überprüfen, ob eine Methode in einem Modul / Paket Anyals Argument für eine der darin definierten Methoden akzeptiert wird, könnte etwas wie der folgende Code verwendet werden (ich habe es nicht ausführlich getestet, da ich es gerade aufgeschrieben habe, aber es scheint meistens mögliche Fälle abdecken):

function check_declared(m::Module, f::Function)
    for mf in methods(f).ms
        if mf.module == m
            if mf.sig isa UnionAll
                b = mf.sig.body
            else
                b = mf.sig
            end
            x = getfield(b, 3)
            for i in 2:length(x)
                if x[i] == Any
                    println(mf)
                    break
                end
            end
        end
    end
end

function check_declared(m::Module)
    for n in names(m)
        try
            f = m.eval(n)
            if f isa Function
                check_declared(m, f)
            end
        catch
            # modules sometimes return names that cannot be evaluated in their scope
        end
    end
end

Wenn Sie es jetzt auf einem Base.IteratorsModul ausführen, erhalten Sie:

julia> check_declared(Iterators)
cycle(xs) in Base.Iterators at iterators.jl:672
drop(xs, n::Integer) in Base.Iterators at iterators.jl:628
enumerate(iter) in Base.Iterators at iterators.jl:133
flatten(itr) in Base.Iterators at iterators.jl:869
repeated(x) in Base.Iterators at iterators.jl:694
repeated(x, n::Integer) in Base.Iterators at iterators.jl:714
rest(itr::Base.Iterators.Rest, state) in Base.Iterators at iterators.jl:465
rest(itr) in Base.Iterators at iterators.jl:466
rest(itr, state) in Base.Iterators at iterators.jl:464
take(xs, n::Integer) in Base.Iterators at iterators.jl:572

und wenn Sie zB das Paket DataStructures.jl überprüfen, erhalten Sie:

julia> check_declared(DataStructures)
compare(c::DataStructures.LessThan, x, y) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps.jl:66
compare(c::DataStructures.GreaterThan, x, y) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps.jl:67
cons(h, t::LinkedList{T}) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\list.jl:13
dec!(ct::Accumulator, x, a::Number) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:86
dequeue!(pq::PriorityQueue, key) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\priorityqueue.jl:288
dequeue_pair!(pq::PriorityQueue, key) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\priorityqueue.jl:328
enqueue!(s::Queue, x) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\queue.jl:28
findkey(t::DataStructures.BalancedTree23, k) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\balanced_tree.jl:277
findkey(m::SortedDict, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_dict.jl:245
findkey(m::SortedSet, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_set.jl:91
heappush!(xs::AbstractArray, x) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\arrays_as_heaps.jl:71
heappush!(xs::AbstractArray, x, o::Base.Order.Ordering) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\arrays_as_heaps.jl:71
inc!(ct::Accumulator, x, a::Number) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:68
incdec!(ft::FenwickTree{T}, left::Integer, right::Integer, val) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\fenwick.jl:64
nil(T) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\list.jl:15
nlargest(acc::Accumulator, n) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:161
nsmallest(acc::Accumulator, n) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:175
reset!(ct::Accumulator{#s14,V} where #s14, x) where V in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\accumulator.jl:131
searchequalrange(m::SortedMultiDict, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\sorted_multi_dict.jl:226
searchsortedafter(m::Union{SortedDict, SortedMultiDict, SortedSet}, k_) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\tokens2.jl:154
sizehint!(d::RobinDict, newsz) in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\robin_dict.jl:231
update!(h::MutableBinaryHeap{T,Comp} where Comp, i::Int64, v) where T in DataStructures at D:\AppData\.julia\packages\DataStructures\iymwN\src\heaps\mutable_binary_heap.jl:250

Was ich vorschlage, ist keine vollständige Lösung für Ihre Frage, aber ich fand es nützlich für mich selbst und dachte darüber nach, es zu teilen.

BEARBEITEN

Der obige Code akzeptiert fwird Functionnur. Im Allgemeinen können Sie Typen haben, die aufgerufen werden können. Dann check_declared(m::Module, f::Function)könnte die Signatur in geändert werden check_declared(m::Module, f)(tatsächlich würde dann die Funktion selbst Anyals zweites Argument zulassen :)) und alle ausgewerteten Namen an diese Funktion übergeben. Dann müssten Sie prüfen, ob die Funktion methods(f)positiv lengthist (wie methodsbei nicht aufrufbaren Werten einen Wert mit Länge zurückgibt 0).

Bogumił Kamiński
quelle