メモ(整理してない)。

ここと、ここを見る。

型の型

任意の値?xに対して、

@assert typeof(x) isa Type

そして、

julia> subtypes(Type)
4-element Vector{Any}:
 Core.TypeofBottom
 DataType
 Union
 UnionAll

型の包含関係

演算子(関数)<:>:で定義される。

値が持つのがConcrete Type、それ以外がAbstract Typeになる。

型とその包含関係で木構造をなしている。 Concrete Typeはその木構造に置いて、必ず末端(葉)にあたり、Abstract Typeは必ず葉以外にあたる。

根にあたるのが、Any

したがって、

  1. 任意の型Tに対して、T<:Any
  2. 任意の具象型T, Sに対して、T<:S == falseかつS<:T == false

具象型かを判定するのはisabstracttype、抽象型かを判定するのはisconcretetype

型の束

実は、Union{}という型は、任意の型Tの部分型。 型は$Types, <:$をなしており、最大元(top)がAny、最小元(bottom)がUnion{}になっている。

julia> Union{} <: Array <: Any
true

julia> Union{} <: Tuple <: Any
true

julia> Union{} <: Tuple{Int64, Float64} <: Any
true

julia> Union{} <: Union{Int64} <: Any
true

Union{}の型がCore.TypeofBottom <: Type

上位型を求める関数はsupertypeで、再帰的にAnyまで求めるのはsupertypes

julia> supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)

julia> supertypes(Vector{Int64})
(Vector{Int64}, DenseVector{Int64}, AbstractVector{Int64}, Any)

束なので、束の元$a$, $b$に対して結び(join)$a\vee b$があり、$a \le a \vee b$かつ$b \le a \vee b$が成り立つ。 joinは、型TSの最小の共通の上位型になっている。これを求めるJuliaの関数は、typejoin

julia> typejoin(Vector{Int64}, Matrix{Float64})
Array

julia> typejoin(Int64, Int32)
Signed

julia> typejoin(Int64, Bool)
Integer

julia> typejoin(Int64, Float64)
Real

julia> typejoin(Int64, Complex{Int64})
Number

julia> typejoin(Int64, String)
Any

逆に、交わりを求めるのはtypeintersect。 これは、T<:Sでもない限り、だいたいUnion{}(ただしTuple)の取り扱いは違う。

julia> typeintersect(Float64, Int32)
Union{}

julia> typeintersect(Vector{Int64}, Matrix{Float64})
Union{}

julia> typeintersect(Vector{Int64}, Array)
Vector{Int64} (alias for Array{Int64, 1})

primitive type

ビット数を指定して定義する。 ほぼ組み込み型みたいなもの。 isprimitivetypeで判定する。

内部的には特定のフラグで管理してるっぽい?

DataTypeについて

https://docs.julialang.org/en/v1/manual/types/#man-declared-types

primitive typeとかabstract typeとかで

  • 明示的に宣言されている
  • 名前がある
  • 明示的な上位型がある(普通、型の宣言で何も書かないとAnyになる)
  • パラメーターがあるかもしれない

これらの特徴を持つのがDataType

Every concrete value in the system is an instance of some DataType.

とあるので、任意のxについて、typeof(x) isa DataTypeっぽい。 typeof(x) == TとなるTは具体型だったので、任意の具体型TについてT isa DataTypeらしい。

Typeの部分型について

抽象型・具体型を表す型はない。

はじめに見たように、

julia> subtypes(Type)
4-element Vector{Any}:
 Core.TypeofBottom
 DataType
 Union
 UnionAll

Core.TypeofBottomUnion{}の型で、これのみの型。

DataTypeは既出。Any isa DataType

Unionは和集合。

UnionAllVector{T} where {T}の様にパラメータを持ってるやつ。

(一般に)パラメーターを持つ型

ざっと上げると、

  • Array{T, N} where {T, N}
  • Tuple
  • Union とかがある。

まず、Unionはただ和をとってるだけなので、他2つとちょっと違う。

一般的に、T <: Sであっても、U{T} <: U{S}ではない。 つまり、非変的(invariant)である(共変性と反変性(計算機科学))。 これは、メモリにおける表現が異なっているかららしい。

julia> Vector{Int64} <: Vector{Any}
false

julia> Complex{Float64} <: Complex{Real}
false

だたし、Tupleは共変的(covariant)である。

julia> Tuple{Int64} <: Tuple{Any}
true

UnionAll

julia> fieldnames(UnionAll)
(:var, :body)

julia> dump(Vector)
UnionAll
  var: TypeVar
    name: Symbol T
    lb: Union{}
    ub: Any
  body: Array{T, 1} <: DenseArray{T, 1}
    ref::MemoryRef{T}
    size::Tuple{Int64}

julia> dump(Vector{<:Real})
UnionAll
  var: TypeVar
    name: Symbol #s36
    lb: Union{}
    ub: Real <: Number
  body: Array{var"#s36"<:Real, 1} <: DenseArray{var"#s36"<:Real, 1}
    ref::MemoryRef{var"#s36"}
    size::Tuple{Int64}

:bodyが本体で、:varはその中の自由変数。 :varTypeVarで、lbubが下界と上界を表してる。 大抵の場合下界はUnion{}

Vector{<:Real}の例は、Vector{var"#s36"} where {var"#s36" <: Real}に脱糖されることがわかる。

メイン: typejoinの実装を見る

実装は、ここ

ここまでの話が分かれば読めるかも?

TypeVarのとき

そのTypeVarの上界ubとのtypejoin

片方がもう片方の部分型のとき

上位型のほう

UnionAllのとき

T{S} where {S}Uだと、typejoin{T, U}{S} where {S}

片方がUnionのとき

Unionの中身のtypejoinともう片方とのtypejoin

julia> typejoin(Union{Int64, String}, Float64)
Any

両方DataTypeのとき

ここまでで、残ってるのはこの場合のみ

まずinferrencebarrierなるものが使われてる。 #44390

片方がTupleのとき

もう片方がTupleじゃないとAny

両方Tupleのとき

片方がTupleのとき(パラメーターがないとき)

ここはTupleの特殊なケースで、Tuple{T} <: Tupleとなるために、Tupleは実はTuple{Vararg{Any}}になってるというやつが利いてくる。 もう片方のTuple{T...}の中身のtypejoinをとってそれのVarargをパラメーターに持つTupleを返す。

両方ただのTupleではないとき

パラメーターの数が同じ時は、順番にtypejoinをとっていく。

片方が多いときは、多い分のtypejoinVarargをいれる(Varargは0個でもOkなので)。

両方Tupleじゃないとき

メイン?最後に残ったもの

https://github.com/JuliaLang/julia/blob/v1.11.2/base/promotion.jl#L95 から始まる

まず、大体a<:bになるまで、bの上位型を遡っていく。 b.name.wrapperは本体を見つけるやつ。 例:

julia> Vector{Int64}.name.wrapper
Array

julia> ComplexF64.name.wrapper
Complex

julia> Int64.name.wrapper
Int64

次にaの方も本体がbと同じになるまで遡る。

if a.name === Type.body.nameType{Int64}みたいなやつかどうかの判定。 そのなかは、Typeになる場合のパターン。

julia> typejoin(Type{T} where {T}, Type{Int64})
Type

julia> typejoin(Type{T} where {T<:Real}, Type{Int64})
Type{T} where T<:Real

julia> typejoin(Type{T} where {T<:Real}, Type{<:Any})
Type

そのあとは、T.parametertypejoinを考えることはない。なぜなら、残ったケースではパラメータにかんしてinvariantだからそのjoinを考える必要がないため。

ここで、複数パラメーターがある型は、一つずつ追加していくのの糖衣構文。 例えば、Array{T, N}は、Array{T}{N}(dump(Array)をみてもわかる。)。

それを踏まえたうえで、パラメータを比較しているところ(l.118)は、一致してたらそのパラメータを共通部分として追加する感じになっている。 言葉だとうまく書けなかったので、例: Array{Int64, 1}Array{Int64, 2}だと、最初のパラメータが一致するので、この段階でArray{Int64}{N} where {N}になる。

一致してなかったら、制限なし(i.e., ubAnylbBottom)のTypeVarvarsに追加してってる。 制限なしのTypeVarをとるのは、aprimary.var(ここでこれでできるのは前述の通りUnionAllTypeVar1つずつでネストされているから)。

どちらにせよ、一つ内側の.bodyを新たにaprimaryにして次のパラメータのループに移る。

で、最後に自由な変数をUnionAllで再びまとめて終わり。

ここら辺がきいてくる例は多分これ

julia> typejoin(Vector{Int64}, Matrix{Int64})
Array{Int64}

TypeVarが具象型なので、あまり例はないはず。

おまけ:メソッドのシグニチャ

(::Method).sigで取れる。

julia> methods(sin)[5]
sin(::Irrational{})
     @ Base.MathConstants mathconstants.jl:146

julia> methods(sin)[5].sig
Tuple{typeof(sin), Irrational{}}

見たらわかる通り、Tuple{typeof(:func), arg1_type, arg2_type, ...}がそれ。

function f(x)::Int64
end

みたいな返り値の型は出てこない。 なぜならこれは返り値を返す直前にconvert(Int64, return_val)する糖衣構文でしかないため。

julia> f(x::T, y::T) where {T<:Real} = 1
f (generic function with 1 methods)

julia> methods(f)[1].sig
Tuple{typeof(f), T, T} where T<:Real

のように、UnionAllになることもある。

おわりに

複雑だけどなんとなくわかった。

今は型の類似度みたいなのがほしいが、それが<:のようにinvariantであるかは少し議論の余地があるなと思った。 (というより、invariantでないほうが直感にあう)