Exiger une déclaration de type dans Julia

16

Existe-t-il un moyen d'exiger explicitement dans Julia (par exemple dans un module ou un package) que les types doivent être déclarés ? Existe-t-il, par exemple, PackageCompilerou Lint.jla-t-il un support pour de tels contrôles? Plus largement, la distribution standard Julia fournit-elle elle-même un analyseur de code statique ou équivalent qui pourrait aider à vérifier cette exigence?

Par exemple, disons que nous voulons nous assurer que notre base de code de production croissante n'accepte que du code qui est toujours de type déclaré , sous l'hypothèse que les grandes bases de code avec des déclarations de type ont tendance à être plus maintenables.

Si nous voulons appliquer cette condition, Julia dans sa distribution standard fournit-elle des mécanismes pour exiger la déclaration de type ou aider à faire avancer cet objectif? (par exemple, tout ce qui pourrait être vérifié via des linters, des hooks de validation ou l'équivalent?)

Amelio Vazquez-Reina
la source
1
Je ne sais pas combien cela aide, mais, comme dans les pensées de Bogumil, hasmethod(f, (Any,) )il reviendra falsesi aucun générique n'a été défini. Vous devez cependant toujours faire correspondre le nombre d'arguments (c'est- hasmethod(f, (Any,Any) )à- dire pour une fonction à deux arguments).
Tasos Papastylianou

Réponses:

9

La réponse courte est: non, il n'y a actuellement aucun outil pour vérifier le type de votre code Julia. C'est possible en principe, cependant, et un certain travail a été fait dans ce sens dans le passé, mais il n'y a pas de bonne façon de le faire pour le moment.

La réponse la plus longue est que les "annotations de type" sont un hareng rouge ici, ce que vous voulez vraiment, c'est la vérification de type, donc la partie la plus large de votre question est en fait la bonne question. Je peux parler un peu de la raison pour laquelle les annotations de type sont un hareng rouge, d'autres choses qui ne sont pas la bonne solution et à quoi ressemblerait le bon type de solution.

Exiger des annotations de type n'atteint probablement pas ce que vous voulez: on pourrait simplement mettre ::Anyn'importe quel champ, argument ou expression et il aurait une annotation de type, mais pas une qui vous dit ou qui compile quoi que ce soit d'utile sur le type réel de cette chose. Il ajoute beaucoup de bruit visuel sans ajouter réellement d'informations.

Pourquoi ne pas exiger des annotations de type concret? Cela exclut de tout mettre ::Any(ce que Julia fait implicitement de toute façon). Cependant, il existe de nombreuses utilisations parfaitement valides des types abstraits que cela rendrait illégaux. Par exemple, la définition de la identityfonction est

identity(x) = x

Quelle annotation de type concret mettriez-vous xsous cette exigence? La définition s'applique à tout x, quel que soit le type — c'est en quelque sorte le but de la fonction. La seule annotation de type correcte est x::Any. Ce n'est pas une anomalie: il existe de nombreuses définitions de fonctions qui nécessitent des types abstraits pour être correctes, donc les forcer à utiliser des types concrets serait assez limitant en termes de type de code Julia que l'on peut écrire.

Il y a une notion de «stabilité de type» dont on parle souvent dans Julia. Le terme semble provenir de la communauté Julia, mais a été repris par d'autres communautés linguistiques dynamiques, comme R. C'est un peu délicat à définir, mais cela signifie en gros que si vous connaissez les types concrets des arguments d'une méthode, vous connaissez également le type de sa valeur de retour. Même si une méthode est de type stable, ce n'est pas tout à fait suffisant pour garantir qu'elle vérifierait le type car la stabilité de type ne parle pas de règles pour décider si quelque chose vérifie ou non. Mais cela va dans la bonne direction: vous voudriez pouvoir vérifier que chaque définition de méthode est de type stable.

Beaucoup ne veulent pas exiger la stabilité du type, même si cela est possible. Depuis Julia 1.0, il est devenu courant d'utiliser de petits syndicats. Cela a commencé avec la refonte du protocole d'itération, qui utilise désormais nothingpour indiquer que l'itération est effectuée par rapport au retour d'un (value, state)tuple lorsqu'il y a plus de valeurs à itérer. Les find*fonctions de la bibliothèque standard utilisent également une valeur de retour de nothingpour indiquer qu'aucune valeur n'a été trouvée. Ce sont des instabilités de type technique, mais elles sont intentionnelles et le compilateur est assez bon pour les raisonner en optimisant autour de l'instabilité. Donc, au moins les petits syndicats doivent probablement être autorisés dans le code. De plus, il n'y a pas de place claire pour tracer la ligne. Bien que l'on puisse peut-être dire qu'un type de retour deUnion{Nothing, T} est acceptable, mais rien de plus imprévisible que cela.

Ce que vous voulez probablement vraiment, cependant, plutôt que d'exiger des annotations de type ou la stabilité de type, c'est d'avoir un outil qui vérifiera que votre code ne peut pas générer d'erreurs de méthode, ou peut-être plus largement qu'il ne générera aucune sorte d'erreur inattendue. Le compilateur peut souvent déterminer avec précision quelle méthode sera appelée sur chaque site d'appel, ou au moins la restreindre à quelques méthodes. C'est ainsi qu'il génère du code rapide - la répartition dynamique complète est très lente (beaucoup plus lente que vtables en C ++, par exemple). Si vous avez écrit un code incorrect, en revanche, le compilateur peut émettre une erreur inconditionnelle: le compilateur sait que vous avez fait une erreur mais ne vous le dit qu'au moment de l'exécution car ce sont les sémantiques du langage. On pourrait exiger que le compilateur puisse déterminer quelles méthodes peuvent être appelées sur chaque site d'appel: cela garantirait que le code sera rapide et qu'il n'y a aucune erreur de méthode. C'est ce qu'un bon outil de vérification de type pour Julia devrait faire. Il y a une bonne base pour ce genre de chose puisque le compilateur fait déjà une grande partie de ce travail dans le cadre du processus de génération de code.

StefanKarpinski
la source
12

C'est une question intéressante. La question clé est ce que nous définissons comme type déclaré . Si vous voulez dire qu'il y a une ::SomeTypedéclaration dans chaque définition de méthode, il est quelque peu difficile à faire car vous avez différentes possibilités de génération de code dynamique dans Julia. Il y a peut-être une solution complète dans ce sens mais je ne la connais pas (j'adorerais l'apprendre).

La chose qui me vient à l'esprit cependant, qui semble relativement plus simple à faire, est de vérifier si une méthode définie dans un module accepte Anycomme argument. Ceci est similaire mais pas équivalent à la déclaration précédente:

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

la même methodsfonction pour la fonction que la signature des deux fonctions accepte xcomme Any.

Maintenant, pour vérifier si une méthode dans un module / package accepte Anycomme argument pour l'une des méthodes définies dans celui-ci, quelque chose comme le code suivant pourrait être utilisé (je ne l'ai pas testé de manière approfondie car je viens de l'écrire, mais il semble couvrir les cas possibles):

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

Maintenant, lorsque vous l'exécutez sur le Base.Iteratorsmodule, vous obtenez:

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

et lorsque vous vérifiez par exemple le package DataStructures.jl, vous obtenez:

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

Ce que je propose n'est pas une solution complète à votre question mais je l'ai trouvée utile pour moi donc j'ai pensé à la partager.

ÉDITER

Le code ci-dessus accepte fd'être Functionuniquement. En général, vous pouvez avoir des types pouvant être appelés. Ensuite, la check_declared(m::Module, f::Function)signature pourrait être modifiée en check_declared(m::Module, f)(en fait, la fonction elle-même le permettrait Anycomme deuxième argument :)) et passer tous les noms évalués à cette fonction. Ensuite, vous devrez vérifier si methods(f)a positif lengthdans la fonction (comme methodspour les non-callables renvoie une valeur qui a une longueur 0).

Bogumił Kamiński
la source