Requerir declaración de tipo en Julia

16

¿Hay alguna forma de exigir explícitamente en Julia (por ejemplo, dentro de un módulo o paquete) que los tipos deben declararse ? ¿Por ejemplo, PackageCompilero Lint.jltiene algún soporte para tales controles? En términos más generales, ¿la distribución estándar de Julia proporciona algún analizador de código estático o equivalente que pueda ayudar a verificar este requisito?

Como un ejemplo motivador, digamos que queremos asegurarnos de que nuestra creciente base de códigos de producción solo acepte códigos que siempre sean de tipo declarado , bajo la hipótesis de que las bases de código grandes con declaraciones de tipo tienden a ser más fáciles de mantener.

Si queremos hacer cumplir esa condición, ¿proporciona Julia en su distribución estándar algún mecanismo para exigir una declaración de tipo o ayudar a avanzar en ese objetivo? (por ejemplo, ¿algo que pueda verificarse a través de linters, commit hooks o equivalente?)

Amelio Vazquez-Reina
fuente
1
No estoy seguro de cuánto ayuda esto, pero, similar a los pensamientos de Bogumil, hasmethod(f, (Any,) )volverá falsesi no se ha definido un genérico. Sin embargo, aún necesitaría hacer coincidir el número de argumentos (es decir, hasmethod(f, (Any,Any) )para una función de dos argumentos).
Tasos Papastylianou

Respuestas:

9

La respuesta corta es: no, actualmente no hay herramientas para el tipo que verifica su código Julia. Sin embargo, es posible en principio, y en el pasado se ha trabajado en esta dirección, pero no hay una buena manera de hacerlo en este momento.

La respuesta más larga es que las "anotaciones de tipo" son una pista falsa aquí, lo que realmente quiere es la verificación de tipo, por lo que la parte más amplia de su pregunta es en realidad la pregunta correcta. Puedo hablar un poco sobre por qué las anotaciones de tipo son un arenque rojo, algunas otras cosas que no son la solución correcta y cómo se vería el tipo correcto de solución.

Requerir anotaciones de tipo probablemente no logre lo que desea: uno podría poner ::Anycualquier campo, argumento o expresión y tendría una anotación de tipo, pero no una que le diga a usted o al compilador algo útil sobre el tipo real de esa cosa. Agrega mucho ruido visual sin agregar ninguna información.

¿Qué hay de requerir anotaciones de tipo concreto? Eso descarta simplemente ponerse ::Anytodo (que es lo que Julia hace implícitamente de todos modos). Sin embargo, hay muchos usos perfectamente válidos de tipos abstractos que esto haría ilegal. Por ejemplo, la definición de la identityfunción es

identity(x) = x

¿Qué tipo de anotación concreta pondrías xbajo este requisito? La definición se aplica a cualquiera x, independientemente del tipo, ese es el punto de la función. La única anotación de tipo que es correcta es x::Any. Esto no es una anomalía: hay muchas definiciones de funciones que requieren tipos abstractos para ser correctas, por lo que obligar a aquellos a usar tipos concretos sería bastante limitante en términos de qué tipo de código de Julia se puede escribir.

Hay una noción de "estabilidad tipográfica" de la que a menudo se habla en Julia. El término parece haberse originado en la comunidad de Julia, pero ha sido recogido por otras comunidades lingüísticas dinámicas, como R. Es un poco difícil de definir, pero significa aproximadamente que si conoce los tipos concretos de los argumentos de un método, También conoce el tipo de su valor de retorno. Incluso si un método es de tipo estable, eso no es suficiente para garantizar que escriba la verificación porque la estabilidad de tipo no habla de ninguna regla para decidir si algo tipo de verificación o no. Pero esto va en la dirección correcta: le gustaría poder verificar que cada definición de método sea de tipo estable.

Es posible que no desee exigir estabilidad de tipo, incluso si pudiera. Desde Julia 1.0, se ha vuelto común el uso de pequeños sindicatos. Esto comenzó con el rediseño del protocolo de iteración, que ahora se usa nothingpara indicar que la iteración se realiza en lugar de devolver una (value, state)tupla cuando hay más valores para iterar. Las find*funciones en la biblioteca estándar también usan un valor de retorno de nothingpara indicar que no se ha encontrado ningún valor. Estas son inestabilidades de tipo técnico, pero son intencionales y el compilador es bastante bueno para razonar acerca de cómo optimizar la inestabilidad. Entonces, al menos, los sindicatos pequeños probablemente deben estar permitidos en el código. Además, no hay un lugar claro para dibujar la línea. Aunque quizás se podría decir que un tipo de retorno deUnion{Nothing, T} es aceptable, pero no hay nada más impredecible que eso.

Sin embargo, lo que probablemente realmente desee, en lugar de requerir anotaciones de tipo o estabilidad de tipo, es tener una herramienta que verifique que su código no pueda arrojar errores de método, o tal vez en términos más generales que no arroje ningún tipo de error inesperado. El compilador a menudo puede determinar con precisión qué método se llamará en cada sitio de llamada, o al menos limitarlo a un par de métodos. Así es como genera código rápido: el despacho dinámico completo es muy lento (mucho más lento que vtables en C ++, por ejemplo). Si ha escrito un código incorrecto, por otro lado, el compilador puede emitir un error incondicional: el compilador sabe que cometió un error, pero no le dice hasta el tiempo de ejecución, ya que esas son las semánticas del lenguaje. Se podría requerir que el compilador pueda determinar qué métodos podrían llamarse en cada sitio de llamada: eso garantizaría que el código sea rápido y que no haya errores de método. Eso es lo que debería hacer una buena herramienta de verificación de tipos para Julia. Hay una gran base para este tipo de cosas ya que el compilador ya hace gran parte de este trabajo como parte del proceso de generación de código.

StefanKarpinski
fuente
12

Esta es una pregunta interesante. La pregunta clave es qué definimos como tipo declarado . Si quiere decir que hay una ::SomeTypedeclaración en cada definición de método, entonces es algo difícil de hacer, ya que tiene diferentes posibilidades de generación de código dinámico en Julia. Quizás haya una solución completa en este sentido, pero no lo sé (me encantaría aprenderlo).

Sin embargo, lo que me viene a la mente, que parece relativamente más simple, es verificar si algún método definido dentro de un módulo acepta Anycomo argumento. Esto es similar pero no equivalente a la declaración 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

se ve igual para la methodsfunción ya que la firma de ambas funciones acepta xcomo Any.

Ahora para verificar si algún método en un módulo / paquete acepta Anycomo argumento a cualquiera de los métodos definidos en él, se podría usar algo como el siguiente código (no lo he probado exhaustivamente ya que lo acabo de escribir, pero parece que en su mayoría cubrir posibles 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

Ahora cuando lo ejecutas en el Base.Iteratorsmódulo obtienes:

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

y cuando, por ejemplo, comprueba el paquete DataStructures.jl obtiene:

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

Lo que propongo no es una solución completa a su pregunta, pero me pareció útil, así que pensé en compartirla.

EDITAR

El código anterior acepta fser Functionsolo. En general, puede tener tipos que se pueden llamar. Luego, la check_declared(m::Module, f::Function)firma podría cambiarse a check_declared(m::Module, f)(en realidad, la función en sí misma permitiría Anycomo segundo argumento :)) y pasar todos los nombres evaluados a esta función. Luego, tendría que verificar si methods(f)tiene un resultado positivo lengthdentro de la función (en cuanto methodsa los valores no invocables devuelve un valor que tiene longitud 0).

Bogumił Kamiński
fuente