Richiesta di dichiarazione del tipo in Julia


16

Esiste un modo per richiedere esplicitamente in Julia (ad esempio all'interno di un modulo o pacchetto) che i tipi debbano essere dichiarati ? Ad esempio PackageCompilero Lint.jlha qualche supporto per tali controlli? Più in generale, la stessa distribuzione standard Julia fornisce analizzatori di codice statici o equivalenti che potrebbero aiutare a verificare questo requisito?

Come esempio motivante, supponiamo che vogliamo assicurarci che la nostra base di codice di produzione in crescita accetti solo codice che è sempre di tipo dichiarato , con l'ipotesi che basi di codice di grandi dimensioni con dichiarazioni di tipo tendano a essere più mantenibili.

Se vogliamo far rispettare tale condizione, Julia nella sua distribuzione standard fornisce meccanismi per richiedere la dichiarazione di tipo o aiutare a raggiungere questo obiettivo? (ad es. qualcosa che potrebbe essere controllato tramite linter, hook di commit o equivalenti?)


1
non sono sicuro di quanto ciò aiuti, ma, simile ai pensieri di Bogumil, hasmethod(f, (Any,) )tornerà falsese non è stato definito alcun generico. Dovresti comunque abbinare il numero di argomenti (ad es. hasmethod(f, (Any,Any) )Per una funzione a due argomenti).
Tasos Papastylianou,

Risposte:


9

La risposta breve è: no, al momento non esistono strumenti per la verifica del tipo del codice Julia. È possibile in linea di principio, tuttavia, e un po 'di lavoro è stato fatto in questa direzione in passato, ma non c'è un buon modo per farlo in questo momento.

La risposta più lunga è che le "annotazioni di tipo" sono un'aringa rossa qui, quello che vuoi davvero è il controllo del tipo, quindi la parte più ampia della tua domanda è in realtà la domanda giusta. Posso parlare un po 'del motivo per cui le annotazioni dei tipi sono un'aringa rossa, alcune altre cose che non sono la soluzione giusta e che aspetto avrebbe il giusto tipo di soluzione.

La richiesta di annotazioni di tipo probabilmente non realizza ciò che desideri: si potrebbe semplicemente mettere ::Anysu qualsiasi campo, argomento o espressione e avrebbe un'annotazione di tipo, ma non quella che dice a te o al compilatore qualcosa di utile sul tipo effettivo di quella cosa. Aggiunge molto rumore visivo senza effettivamente aggiungere alcuna informazione.

Che ne dici di richiedere annotazioni di tipo concreto? Ciò esclude solo il mettere ::Anytutto (che è ciò che Julia fa implicitamente comunque). Tuttavia, ci sono molti usi perfettamente validi di tipi astratti che ciò renderebbe illegale. Ad esempio, la definizione della identityfunzione è

identity(x) = x

Quale tipo di annotazione concreta metteresti su xquesto requisito? La definizione si applica a qualsiasi x, indipendentemente dal tipo, che è il tipo di punto della funzione. L'unica annotazione di tipo corretta è x::Any. Questa non è un'anomalia: ci sono molte definizioni di funzioni che richiedono tipi astratti per essere corretti, quindi forzare quelli ad usare tipi concreti sarebbe piuttosto limitante in termini di che tipo di codice Julia si può scrivere.

C'è una nozione di "stabilità del tipo" di cui si parla spesso in Julia. Il termine sembra aver avuto origine nella comunità Julia, ma è stato ripreso da altre comunità linguistiche dinamiche, come R. È un po 'difficile da definire, ma significa approssimativamente che se conosci i tipi concreti degli argomenti di un metodo, conosci anche il tipo del suo valore di ritorno. Anche se un metodo è di tipo stabile, non è abbastanza per garantire che verifichi il controllo di tipo perché la stabilità di tipo non parla di alcuna regola per decidere se qualcosa di tipo controlla o meno. Ma questo sta andando nella giusta direzione: ti piacerebbe essere in grado di verificare che ogni definizione di metodo sia di tipo stabile.

Molti non vogliono richiedere la stabilità del tipo, anche se si potesse. Da Julia 1.0, è diventato comune usare piccoli sindacati. Questo è iniziato con la riprogettazione del protocollo di iterazione, che ora utilizza nothingper indicare che l'iterazione viene eseguita anziché restituire una (value, state)tupla quando ci sono più valori da iterare. Le find*funzioni nella libreria standard usano anche un valore di ritorno di nothingper indicare che non è stato trovato alcun valore. Queste sono instabilità di tipo tecnico, ma sono intenzionali e il compilatore è abbastanza bravo a ragionare su di loro ottimizzando l'instabilità. Quindi almeno i piccoli sindacati probabilmente devono essere ammessi nel codice. Inoltre, non esiste un posto chiaro per tracciare la linea. Anche se forse si potrebbe dire che un tipo di ritorno diUnion{Nothing, T} è accettabile, ma niente di più imprevedibile di così.

Ciò che probabilmente vuoi davvero, invece di richiedere annotazioni di tipo o stabilità di tipo, è avere uno strumento che verifichi che il tuo codice non può generare errori di metodo o forse più in generale che non genererà alcun tipo di errore imprevisto. Il compilatore può spesso determinare con precisione quale metodo verrà chiamato in ciascun sito di chiamata, o almeno restringerlo a un paio di metodi. Questo è il modo in cui genera codice veloce: l'invio dinamico completo è molto lento (molto più lento di vtables in C ++, per esempio). Se hai scritto un codice errato, d'altra parte, il compilatore potrebbe emettere un errore incondizionato: il compilatore sa che hai fatto un errore ma non ti dice fino al runtime poiché quelli sono la semantica del linguaggio. Si potrebbe richiedere che il compilatore sia in grado di determinare quali metodi potrebbero essere chiamati in ciascun sito di chiamata: ciò garantirebbe che il codice sarà veloce e che non ci siano errori di metodo. Questo è ciò che dovrebbe fare un buon strumento di controllo del tipo per Julia. C'è una grande base per questo genere di cose poiché il compilatore fa già gran parte di questo lavoro come parte del processo di generazione del codice.


12

Questa è una domanda interessante La domanda chiave è cosa definiamo tipo dichiarato . Se vuoi dire che c'è ::SomeTypeun'istruzione in ogni definizione di metodo, allora è un po 'complicato da fare poiché hai diverse possibilità di generazione di codice dinamico in Julia. Forse esiste una soluzione completa in questo senso ma non la conosco (mi piacerebbe impararla).

La cosa che mi viene in mente, che sembra relativamente più semplice da fare è verificare se qualsiasi metodo definito all'interno di un modulo accetta Anycome argomento. Questo è simile ma non equivalente all'affermazione precedente come:

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

lo stesso aspetto per methodsfunzione la firma di entrambe le funzioni accetta xcome Any.

Ora per verificare se un metodo in un modulo / pacchetto accetta Anycome argomento uno qualsiasi dei metodi definiti in esso, si potrebbe usare qualcosa come il seguente codice (non l'ho testato ampiamente come l'ho appena scritto, ma sembra principalmente coprire possibili casi):

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

Ora quando lo esegui sul Base.Iteratorsmodulo ottieni:

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

e quando ad esempio controlli il pacchetto DataStructures.jl ottieni:

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

Quello che propongo non è una soluzione completa alla tua domanda, ma l'ho trovato utile per me stesso, quindi ho pensato di condividerlo.

MODIFICARE

Il codice sopra accetta fdi essere Functionsolo. In generale puoi avere tipi che possono essere richiamabili. Quindi la check_declared(m::Module, f::Function)firma potrebbe essere cambiata in check_declared(m::Module, f)(in realtà la funzione stessa consentirebbe Anycome secondo argomento :)) e passare tutti i nomi valutati a questa funzione. Quindi dovresti verificare se methods(f)ha una lengthfunzione positiva all'interno della funzione (come methodsper i valori non richiamabili restituisce un valore che ha lunghezza 0).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.