要求在 Julia 中进行类型声明



有没有办法在Julia中明确要求(例如在模块或包中)必须声明类型?例如PackageCompilerLint.jl支持此类检查?更广泛地说,Julia 标准发行版本身是否提供任何静态代码分析器或等效项来帮助检查此要求?

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

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

简短的回答是:不,目前没有用于类型检查 Julia 代码的工具。然而,原则上这是可能的,过去已经在这个方向上做了一些工作,但现在没有一个好的方法可以做到这一点。

更长的答案是"类型注释"在这里是一个红鲱鱼,你真正想要的是类型检查,所以你的问题更广泛的部分实际上是正确的问题。我可以谈谈为什么类型注释是一个红鲱鱼,其他一些不是正确解决方案的东西,以及正确的解决方案会是什么样子。

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

需要具体类型注释怎么样?这排除了把::Any放在所有事情上(无论如何,朱莉娅都隐含地这样做了)。但是,抽象类型的许多完全有效的用法会使它们成为非法的。例如,identity函数的定义是

identity(x) = x

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

在Julia中经常谈论"类型稳定性"的概念。这个词似乎起源于Julia社区,但已经被其他充满活力的语言社区所接受,比如R。定义起来有点棘手,但它大致意味着,如果您知道方法参数的具体类型,您也知道其返回值的类型。即使一个方法是类型稳定的,这也不足以保证它会进行类型检查,因为类型稳定性不讨论任何用于决定某些东西是否进行类型检查的规则。但这正朝着正确的方向发展:您希望能够检查每个方法定义是否类型稳定。

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

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

这是一个有趣的问题。关键问题是我们将什么定义为声明的类型。 如果您的意思是每个方法定义中都有一个::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

现在要检查模块/包中的任何方法是否接受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.juliapackagesDataStructuresiymwNsrcheaps.jl:66
compare(c::DataStructures.GreaterThan, x, y) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcheaps.jl:67
cons(h, t::LinkedList{T}) where T in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrclist.jl:13
dec!(ct::Accumulator, x, a::Number) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcaccumulator.jl:86
dequeue!(pq::PriorityQueue, key) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcpriorityqueue.jl:288
dequeue_pair!(pq::PriorityQueue, key) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcpriorityqueue.jl:328
enqueue!(s::Queue, x) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcqueue.jl:28
findkey(t::DataStructures.BalancedTree23, k) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcbalanced_tree.jl:277
findkey(m::SortedDict, k_) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcsorted_dict.jl:245
findkey(m::SortedSet, k_) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcsorted_set.jl:91
heappush!(xs::AbstractArray, x) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcheapsarrays_as_heaps.jl:71
heappush!(xs::AbstractArray, x, o::Base.Order.Ordering) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcheapsarrays_as_heaps.jl:71
inc!(ct::Accumulator, x, a::Number) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcaccumulator.jl:68
incdec!(ft::FenwickTree{T}, left::Integer, right::Integer, val) where T in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcfenwick.jl:64
nil(T) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrclist.jl:15
nlargest(acc::Accumulator, n) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcaccumulator.jl:161
nsmallest(acc::Accumulator, n) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcaccumulator.jl:175
reset!(ct::Accumulator{#s14,V} where #s14, x) where V in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcaccumulator.jl:131
searchequalrange(m::SortedMultiDict, k_) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcsorted_multi_dict.jl:226
searchsortedafter(m::Union{SortedDict, SortedMultiDict, SortedSet}, k_) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrctokens2.jl:154
sizehint!(d::RobinDict, newsz) in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcrobin_dict.jl:231
update!(h::MutableBinaryHeap{T,Comp} where Comp, i::Int64, v) where T in DataStructures at D:AppData.juliapackagesDataStructuresiymwNsrcheapsmutable_binary_heap.jl:250

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

编辑

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

最新更新