`Tuple` 中的 `Vararg` 类型参数的疑问

首先定义一些变量

julia> T1 = Tuple{Vararg{T, 2}} where T; T2 = Tuple{Vararg{T, 2} where T};

我感觉应该有 T1 == T2,可是在 Julia 中我得到了以下结果:

julia> T1 == T2
false

julia> T1 <: T2
true

julia> T1 >: T2
false

如果把 Vararg 替换成 Array,倒是会符合我的预测:

julia> (Tuple{Array{T, 2}} where T) == Tuple{Array{T, 2} where T}
true

以上关系成立的原因是什么,或者说这是一个 bug 吗?

不带分号就能看出差别

julia> T1 = Tuple{Vararg{T, 2}} where T
Tuple{Vararg{T,2}} where T

julia> T2 = Tuple{Vararg{T, 2} where T}
Tuple{Any,Any}

julia> T1 == T2
false

julia> (1, 2) isa T1
true

julia> (1, 2) isa T2
true

julia> (1, 2.0) isa T1
false

julia> (1, 2.0) isa T2
true

T1 限定了两个参数必须是一样的类型;而 T2 两个参数的类型可以不一样

我就是对 T2 的值感到奇怪,我以为会等于 Tuple{T, T} where T(与 T1 相等),实际上却等于 Tuple{T, S} where {T, S}(与 Tuple{Any, Any} 相等),T2 定义里的类型约束平白无故就没了。此外,我还发现了一个奇怪的现象,见此 issue

@test isequal_type(Type{Tuple{Vararg{Int,N}} where N}, Type{Tuple{Vararg{Int,N} where N}})
@test Type{Tuple{Vararg{Int,N}} where N} !== Type{Tuple{Vararg{Int,N} where N}}

test 里测试了 where N 的位置是没有影响的,不知道为什么没有测试 where T

目前 v1.1.1 的情况

julia> Type{Tuple{Vararg{Int,N}} where N} == Type{Tuple{Vararg{Int,N} where N}}
true

julia> Type{Tuple{Vararg{T,2}} where T} == Type{Tuple{Vararg{T,2} where T}}
false

let A = Tuple{Vararg{Val{N}, N} where N},
    B = Tuple{Vararg{Val{N}, N}} where N,
    C = Tuple{Val{2}, Val{2}}

    @test isequal_type(A, B)
    @test issub(C, B)
    @test issub(C, A)
end
julia> Tuple{Vararg{Val{N}, N} where N} == Tuple{Vararg{Val{N}, N}} where N
true

julia> Tuple{Vararg{Val{N}, 2} where N} == Tuple{Vararg{Val{N}, 2}} where N
false

julia> Tuple{Vararg{Val{2}, 2}} == Tuple{Vararg{Val{2}, 2}}
true

julia> Tuple{Vararg{Val{N}, 2} where N <: Integer} == Tuple{Vararg{Val{N}, 2}} where N <: Integer
false

不是很懂


又看到一个相关的测试,不过没搞清楚 @testintersect 宏的功能

    # part of issue #20344
    @testintersect(Tuple{Type{Tuple{Vararg{T, N} where N}}, Tuple} where T,
                   Tuple{Type{Tuple{Vararg{T, N}}} where N where T, Any},
                   Bottom)

T2的约束是有的,如果定义成T2 = Tuple{Vararg{T, 2} where T<:Real};
那么T2的两个元素必须为Real,可能是两个int的组合,也可能是int和float的组合,这点手册里面有说道UnionAll Types,如果是Tuple{T, S} where {T, S},两个元素可一点关联都没有。

楼主想说的是,像 T2 应该和 T1 一样,有两个参数都是同类型的约束。
而不是两个 Any,两个 Any 就可以是不一样的类型(例子见我的第一个回复)。

julia> T2 = Tuple{Vararg{T, 2} where T}
Tuple{Any,Any}

我最近也遇到了类似的问题,梳理下,也许对其它人有用。

假如我想定义一个函数,可以接受任意长度任意类型的positional arguments,那么可以这么定义:

f(x...) = nothing

其实也就等效于

f(x::Any...) = nothing

表明这里的每个参数都是Any类型。

假如我希望,每个参数都需要有相同的类型,那么,一般来说会这么写:

julia> g(x::T...) where T = nothing
g (generic function with 1 method)

julia> g(1,2)

julia> g(1,:a)
ERROR: MethodError: no method matching g(::Int64, ::Symbol)
Closest candidates are:
  g(::T...) where T at REPL[6]:1
Stacktrace:
 [1] top-level scope at none:0

而不会这么写:

julia> h(x::T where T...) = nothing
ERROR: syntax: invalid variable expression in "where"

上面可以看到,这么写其实都会报错。

不过,前面我们使用了...这个语法,其实是可以拿掉的,也就变成了Vararg。接下来看下有什么不同:

如果想接收任意个数的positional parameter,那么可以直接这么定义:

julia> m(x::Vararg) = x
m (generic function with 1 method)

julia> m
m (generic function with 1 method)

julia> methods(m)
# 1 method for generic function "m":
[1] m(x...) in Main at REPL[24]: 

如果希望所有的参数都有相同的类型,那么这样写:

julia> n(x::Vararg{T}) where T = x
n (generic function with 1 method)

julia> methods(n)
# 1 method for generic function "n":
[1] n(x::T...) where T in Main at REPL[27]: 

:sleepy: 终于到了这个帖子的主题了,问题在于,还有下面这种写法:

julia> o(x::Vararg{T} where T) = x
o (generic function with 1 method)

julia> methods(o)
# 1 method for generic function "o":
[1] o(x...) in Main at REPL[30]:  

可以看到,这里其实等于没有修饰,类似地也就导致了楼主的问题。

为啥这样子呢,反正我还没弄懂

说说我的理解,欢迎轻拍,这里的确很迷惑人。因为我觉得这里涉及到了函数(方法)参数与类型参数(UnionAll类型)混合的事情,那么我们要弄清楚这个参数到底是①函数的参数还是②类型的参数。
我的理解是:

  • 放在函数括弧外面的就是函数的参数
  • 跟在类型后面就是类型参数

第一个要说的是where T等效于where T<:Any,所以Vararg{T} where T 就是Vararg{<:Any}。需要说明的UnionAll的Vararg和NTuple比较奇怪,与一般的UnionAll类型有差别,举例:

julia> Vararg{Int, 4} <: Vararg{Any, 4}
true

julia> Array{Int, 1} <: Array{Any, 1}
false

julia> Array{Int, 1} <: Array{<:Any, 1}
true

上面主要还是扯UnionAll类型
我们现在再看函数参数

t3(x::Vararg{T} where T) = println(typeof(x))
t4(x::Vararg{T})  where T = println(typeof(x))

运行后

julia> t3(1, 2, 'c')
Tuple{Int64,Int64,Char}

julia> t4(1, 2, 'c')
ERROR: MethodError: no method matching t4(::Int64, ::Int64, ::Char)
Closest candidates are:
  t4(::T...) where T at REPL[21]:1
Stacktrace:
 [1] top-level scope at none:0

对于t3函数,其where只是UnionAll类型的表达方式,且Tuple{Int64,Int64,Char} <: Vararg{<:Any}是成立的。

而对于t4,函数参数就必须用一个concrete类型来取代,比如Int,Float64,对应的分别为t4(x::Vararg{Int})和t4(x::Vararg{Float64}),这样每给一个不同的参数,函数被编译一次从而获得该函数的不同方法。这也解释了为什么上面o和n函数不同的行为。

julia> f(x::Vararg{T} where T) = println(typeof(x), T)
f (generic function with 1 method)

julia> f(1)
ERROR: UndefVarError: T not defined

这种写法的类型参数在函数里不可见,所以说它取什么值意义不大。