Exigindo declaração de tipo em Julia

16

Existe alguma maneira de exigir explicitamente em Julia (por exemplo, dentro de um módulo ou pacote) que tipos devem ser declarados ? Por exemplo, PackageCompilerou Lint.jltem algum suporte para tais verificações? Em termos mais gerais, a própria distribuição padrão Julia fornece algum analisador de código estático ou equivalente que possa ajudar a verificar esse requisito?

Como um exemplo motivador, digamos que queremos garantir que nossa base crescente de códigos de produção aceite apenas códigos sempre declarados por tipo , sob a hipótese de que grandes bases de código com declarações de tipo tendem a ser mais sustentáveis.

Se queremos impor essa condição, Julia em sua distribuição padrão fornece algum mecanismo para exigir declaração de tipo ou ajudar a avançar esse objetivo? (por exemplo, algo que possa ser verificado através de linters, ganchos de confirmação ou equivalente?)

Amelio Vazquez-Reina
fonte
11
não tenho certeza de quanto isso ajuda, mas, semelhante aos pensamentos da Bogumil, hasmethod(f, (Any,) )retornará falsese nenhum genérico tiver sido definido. Você ainda precisará corresponder ao número de argumentos ( hasmethod(f, (Any,Any) )por exemplo, para uma função de dois argumentos).
Tasos Papastylianou

Respostas:

9

A resposta curta é: não, atualmente não há ferramentas para verificar o seu código Julia. No entanto, é possível em princípio, e algum trabalho foi feito nessa direção no passado, mas não há uma boa maneira de fazê-lo agora.

A resposta mais longa é que as "anotações de tipo" são um arenque vermelho aqui, o que você realmente deseja é a verificação de tipo; portanto, a parte mais ampla da sua pergunta é realmente a pergunta certa. Posso falar um pouco sobre por que as anotações de tipo são um arenque vermelho, algumas outras coisas que não são a solução certa e como seria o tipo certo de solução.

Exigir anotações de tipo provavelmente não realiza o que você deseja: basta colocar ::Any qualquer campo, argumento ou expressão e teria uma anotação de tipo, mas não uma que informe a você ou ao compilador algo útil sobre o tipo real dessa coisa. Ele adiciona muito ruído visual sem adicionar nenhuma informação.

Que tal exigir anotações de tipo concreto? Isso exclui apenas colocar ::Anytudo (que é o que Julia implicitamente faz de qualquer maneira). No entanto, existem muitos usos perfeitamente válidos de tipos abstratos que isso tornaria ilegal. Por exemplo, a definição da identityfunção é

identity(x) = x

Que anotação de tipo concreto você aplicaria xsob esse requisito? A definição se aplica a qualquer um x, independentemente do tipo - esse é o objetivo da função. A única anotação de tipo que está correta é x::Any. Isso não é uma anomalia: existem muitas definições de funções que exigem tipos abstratos para serem corretas, portanto, forçar aqueles a usar tipos concretos seria bastante limitante em termos de que tipo de código Julia se pode escrever.

Há uma noção de "estabilidade de tipo" que é frequentemente mencionada em Julia. O termo parece ter se originado na comunidade Julia, mas foi escolhido por outras comunidades de linguagem dinâmica, como R. É um pouco complicado de definir, mas significa aproximadamente que, se você conhece os tipos concretos dos argumentos de um método, você conhece o tipo de seu valor de retorno também. Mesmo que um método seja do tipo estável, isso não é suficiente para garantir que ele digite check, porque a estabilidade do tipo não fala sobre nenhuma regra para decidir se algo do tipo verifica ou não. Mas isso está indo na direção certa: você gostaria de poder verificar se cada definição de método é do tipo estável.

Muitos não desejam exigir estabilidade de tipo, mesmo que possam. Desde Julia 1.0, tornou-se comum o uso de pequenos sindicatos. Isso começou com o redesenho do protocolo de iteração, que agora usa nothingpara indicar que a iteração é feita versus o retorno de uma (value, state)tupla quando houver mais valores para iterar. As find*funções na biblioteca padrão também usam um valor de retorno de nothingpara indicar que nenhum valor foi encontrado. Essas são instabilidades do tipo tecnicamente, mas são intencionais e o compilador é muito bom em raciocinar sobre a otimização em torno da instabilidade. Portanto, pelo menos pequenas uniões provavelmente devem ser permitidas no código. Além disso, não há um lugar claro para traçar a linha. Embora talvez se possa dizer que um tipo de retorno deUnion{Nothing, T} é aceitável, mas não é algo mais imprevisível do que isso.

O que você provavelmente realmente deseja, no entanto, em vez de exigir anotações de tipo ou estabilidade de tipo, é ter uma ferramenta que verifique se seu código não pode gerar erros de método, ou talvez de maneira mais ampla, que não gere nenhum tipo de erro inesperado. O compilador geralmente pode determinar com precisão qual método será chamado em cada site de chamada ou pelo menos reduzi-lo a alguns métodos. É assim que gera código rápido - o envio dinâmico completo é muito lento (muito mais lento que o vtables em C ++, por exemplo). Se você escreveu código incorreto, por outro lado, o compilador pode emitir um erro incondicional: o compilador sabe que você cometeu um erro, mas não informa até o tempo de execução, pois essas são as semânticas do idioma. Pode-se exigir que o compilador seja capaz de determinar quais métodos podem ser chamados em cada site de chamada: isso garantiria que o código fosse rápido e que não houvesse erros de método. É isso que uma boa ferramenta de verificação de tipo para Julia deve fazer. Há uma excelente base para esse tipo de coisa, já que o compilador já faz grande parte desse trabalho como parte do processo de geração de código.

StefanKarpinski
fonte
12

Esta é uma pergunta interessante. A questão principal é o que definimos como tipo declarado . Se você quer dizer que existe uma ::SomeTypedeclaração em cada definição de método, é um pouco complicado, pois você tem diferentes possibilidades de geração dinâmica de código em Julia. Talvez exista uma solução completa nesse sentido, mas eu não a conheço (eu adoraria aprender).

O que me vem à mente, porém, que parece relativamente mais simples é verificar se algum método definido dentro de um módulo aceita Anycomo argumento. Isso é semelhante, mas não equivalente à declaração anterior, como:

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

procure o mesmo para a methodsfunção que a assinatura de ambas as funções aceita xcomo Any.

Agora, para verificar se algum método em um módulo / pacote aceita Anycomo argumento para qualquer um dos métodos definidos nele, algo como o código a seguir poderia ser usado (eu não o testei extensivamente porque acabei de escrevê-lo, mas parece cobrir possíveis casos):

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

Agora, quando você o executa no Base.Iteratorsmódulo, você obtém:

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 você verifica o pacote DataStructures.jl, obtém:

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

O que proponho não é uma solução completa para sua pergunta, mas achei útil para mim, então pensei em compartilhá-la.

EDITAR

O código acima aceita fser Functionapenas. Em geral, você pode ter tipos que podem ser chamados. Em seguida, a check_declared(m::Module, f::Function)assinatura pode ser alterada para check_declared(m::Module, f)(na verdade, a própria função permitiria Anycomo o segundo argumento :)) e passar todos os nomes avaliados para essa função. Então você teria que verificar se methods(f)há positivo lengthdentro da função (como methodspara retornos não exigíveis um valor que tenha comprimento 0).

Bogumił Kamiński
fonte