需要在 Julia 中进行类型声明

Requiring type declaration in Julia

有没有办法在 Julia 中明确要求(例如在模块或包中)types must 被宣布?例如PackageCompilerLint.jl 是否支持此类检查?更广泛地说,Julia 标准发行版本身是否提供任何 静态代码分析器 或可帮助检查此要求的等效工具?

作为一个激励性的例子,假设我们想要确保我们不断增长的生产代码库只接受始终类型声明的代码,假设具有类型声明的大型代码库往往更易于维护。

如果我们想要强制执行该条件,Julia 在其标准发行版中是否提供任何机制来要求类型声明或帮助推进该目标? (例如,任何可以通过 linters、提交钩子或等效物检查的东西?)

这是一个有趣的问题。关键问题是我们定义为声明的类型。 如果您的意思是每个方法定义中都有一个 ::SomeType 语句,那么这样做有点棘手,因为您在 Julia 中有不同的动态代码生成可能性。也许在这个意义上有一个完整的解决方案,但我不知道(我很想学习它)。

我想到的事情似乎相对简单,就是检查模块中定义的任何方法是否接受 Any 作为其参数。这类似于但不等同于前面的声明:

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

methods 函数看起来相同,因为两个函数的签名接受 x 作为 Any

现在检查 module/package 中的任何方法是否接受 Any 作为其中定义的任何方法的参数,可以使用类似下面的代码(我没有广泛测试它作为我刚刚写下来了,但它似乎涵盖了大部分可能的情况):

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

现在,当您在 Base.Iterators 模块上 运行 它时,您将获得:

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

当你例如检查 DataStructures.jl 您获得的包裹:

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

我提出的不是您问题的完整解决方案,但我发现它对我自己有用,所以我想分享它。

编辑

上面的代码只接受 fFunction。通常,您可以拥有可调用的类型。然后 check_declared(m::Module, f::Function) 签名可以更改为 check_declared(m::Module, f)(实际上函数本身将允许 Any 作为第二个参数 :))并将所有评估的名称传递给此函数。然后你必须检查 methods(f) 是否在函数内部有正数 length (因为 methods 对于不可调用的 returns 一个长度为 0 的值)。

简短的回答是:不,目前没有用于检查 Julia 代码类型的工具。不过原则上是可以的,过去也有人在这方面做了一些工作,但目前还没有很好的办法。

较长的答案是 "type annotations" 在这里是转移注意力,您真正想要的是类型检查,因此您问题的更广泛部分实际上是正确的问题。我可以谈谈为什么类型注释是转移注意力的问题,其他一些不是正确解决方案的事情,以及正确的解决方案应该是什么样子。

需要类型注释可能无法实现您想要的:可以将 ::Any 放在任何字段、参数或表达式上,它会有一个类型注释,但不会告诉您或编译器关于那个东西的实际类型的任何有用的东西。它在没有实际添加任何信息的情况下增加了很多视觉噪音。

要求具体的类型注释怎么样?这排除了仅将 ::Any 放在所有内容上的可能性(这是 Julia 隐式所做的)。然而,有许多抽象类型的完全有效的使用,这将成为非法的。比如identity函数的定义是

identity(x) = x

根据此要求,您会在 x 上添加什么具体类型注释?该定义适用于任何 x,无论类型如何——这就是函数的要点。唯一正确的类型注释是 x::Any。这不是异常:有许多函数定义需要抽象类型才能正确,因此强制这些函数定义使用具体类型在可以编写什么样的 Julia 代码方面会受到很大限制。

有一个 "type stability" 的概念在 Julia 中经常被谈论。该术语似乎起源于 Julia 社区,但已被其他动态语言社区采用,例如 R。定义它有点棘手,但它大致意味着如果您知道方法参数的具体类型,你也知道它的 return 值的类型。即使一个方法是类型稳定的,也不足以保证它会进行类型检查,因为类型稳定性不涉及任何决定是否进行类型检查的规则。但这是朝着正确的方向发展:您希望能够检查每个方法定义的类型是否稳定。

你们很多人不想要求类型稳定性,即使可以。从 Julia 1.0 开始,使用小联合变得很普遍。这从迭代协议的重新设计开始,它现在使用 nothing 来指示迭代完成,而不是在有更多值要迭代时 returning 一个 (value, state) 元组。标准库中的 find* 函数也使用 nothing 的 return 值来表示没有找到值。从技术上讲,这些是类型不稳定性,但它们是有意为之的,编译器非常善于推理它们围绕不稳定性进行优化。所以至少小联合可能必须在代码中被允许。此外,没有明确的界限。虽然也许有人会说 return 类型的 Union{Nothing, T} 是可以接受的,但没有比这更不可预测的了。

然而,您真正想要的可能不是需要类型注释或类型稳定性,而是拥有一个工具来检查您的代码不会抛出方法错误,或者更广泛地说,它不会抛出任何类型的错误意外的错误。编译器通常可以精确地确定在每个调用点将调用哪个方法,或者至少将其缩小到几个方法。这就是它生成快速代码的方式——完全动态调度非常慢(例如,比 C++ 中的 vtables 慢得多)。另一方面,如果您编写了不正确的代码,编译器可能会发出无条件错误:编译器知道您犯了一个错误,但直到运行时才告诉您,因为这些是语言语义。可以要求编译器能够确定在每个调用点可以调用哪些方法:这将保证代码速度快并且没有方法错误。这就是 Julia 的一个好的类型检查工具应该做的。这类事情有很好的基础,因为编译器已经在生成代码的过程中完成了大部分工作。