More about types

Julia를 사용한 지 오래되었다면, 타입이 수행하는 기본적인 역할을 이해하고 있을 것입니다. 여기에서는 Parametric Types에 특히 초점을 맞추어 내부를 살펴보려고 합니다.

Types and sets (and Any and Union{}/Bottom)

줄리아의 타입 시스템을 집합의 관점에서 이해하는 것이 가장 쉬울 수 있습니다. 프로그램이 개별 값을 조작하는 반면, 타입은 값의 집합을 나타냅니다. 이는 컬렉션과는 다릅니다. 예를 들어, Set 값의 집합은 그 자체로 단일 Set 값입니다. 오히려, 타입은 가능한 값의 집합을 설명하며, 우리가 어떤 값을 가지고 있는지에 대한 불확실성을 표현합니다.

A concrete type Ttypeof 함수에 의해 반환된 직접 태그가 T인 값의 집합을 설명합니다. abstract type은 일부 더 큰 값 집합을 설명합니다.

Any는 가능한 값의 전체 우주를 설명합니다. IntegerAny의 하위 집합으로 Int, Int8 및 기타 구체적인 유형을 포함합니다. 내부적으로 Julia는 Bottom으로 알려진 또 다른 유형을 많이 사용하며, 이는 Union{}로도 작성할 수 있습니다. 이는 공집합에 해당합니다.

줄리아의 타입은 집합 이론의 표준 연산을 지원합니다: T1T2의 "부분집합" (서브타입)인지 T1 <: T2로 물어볼 수 있습니다. 마찬가지로, 두 타입을 교차하려면 typeintersect를 사용하고, 그들의 합집합을 Union로 취하며, 그들의 합집합을 포함하는 타입을 typejoin로 계산합니다:

julia> typeintersect(Int, Float64)
Union{}

julia> Union{Int, Float64}
Union{Float64, Int64}

julia> typejoin(Int, Float64)
Real

julia> typeintersect(Signed, Union{UInt8, Int8})
Int8

julia> Union{Signed, Union{UInt8, Int8}}
Union{UInt8, Signed}

julia> typejoin(Signed, Union{UInt8, Int8})
Integer

julia> typeintersect(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Int64, Float64}

julia> Union{Tuple{Integer, Float64}, Tuple{Int, Real}}
Union{Tuple{Int64, Real}, Tuple{Integer, Float64}}

julia> typejoin(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Integer, Real}

이러한 연산은 추상적으로 보일 수 있지만, 줄리아의 핵심에 자리잡고 있습니다. 예를 들어, 메서드 디스패치는 메서드 목록의 항목을 순회하여 인수 튜플의 유형이 메서드 시그니처의 하위 유형인 항목에 도달할 때까지 진행됩니다. 이 알고리즘이 작동하려면 메서드가 그들의 특이성에 따라 정렬되어야 하며, 검색은 가장 특이한 메서드부터 시작해야 합니다. 따라서 줄리아는 유형에 대한 부분 순서도 구현합니다. 이는 <:와 유사한 기능을 통해 달성되지만, 아래에서 논의될 차이점이 있습니다.

UnionAll types

줄리아의 타입 시스템은 또한 반복된 유니온 타입을 표현할 수 있습니다: 특정 변수의 모든 값에 대한 타입의 유니온입니다. 이는 일부 매개변수의 값이 알려지지 않은 경우 매개변수 타입을 설명하는 데 필요합니다.

예를 들어, ArrayArray{Int,2}와 같이 두 개의 매개변수를 가지고 있습니다. 요소 유형을 모른다면 Array{T,2} where T라고 쓸 수 있으며, 이는 모든 값 T에 대한 Array{T,2}의 합집합입니다: Union{Array{Int8,2}, Array{Int16,2}, ...}.

이러한 유형은 UnionAll 객체로 표현되며, 여기에는 변수(T는 이 예제에서 TypeVar 유형)와 래핑된 유형(Array{T,2}는 이 예제에서) 이 포함됩니다.

다음 방법을 고려하십시오:

f1(A::Array) = 1
f2(A::Array{Int}) = 2
f3(A::Array{T}) where {T<:Any} = 3
f4(A::Array{Any}) = 4

서명 - Function calls에 설명된 대로 - f3의 타입은 튜플 타입을 감싸는 UnionAll 타입입니다: Tuple{typeof(f3), Array{T}} where T. f4를 제외한 모든 함수는 a = [1,2]로 호출할 수 있으며, f2를 제외한 모든 함수는 b = Any[1,2]로 호출할 수 있습니다.

이러한 유형을 좀 더 자세히 살펴보겠습니다:

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

이것은 Array가 실제로 UnionAll 유형의 이름임을 나타냅니다. 매개변수마다 하나의 UnionAll 유형이 중첩되어 있습니다. 구문 Array{Int,2}Array{Int}{2}와 동등합니다. 내부적으로 각 UnionAll은 특정 변수 값으로 한 번에 하나씩, 가장 바깥쪽부터 인스턴스화됩니다. 이는 후행 유형 매개변수를 생략하는 것에 자연스러운 의미를 부여합니다. Array{Int}Array{Int,N} where N과 동등한 유형을 제공합니다.

TypeVar는 그 자체로 타입이 아니라 UnionAll 타입의 구조의 일부로 간주되어야 합니다. 타입 변수는 값에 대한 하한과 상한을 가지고 있습니다(필드 lbub에서). 기호 name은 순전히 장식적입니다. 내부적으로 TypeVar는 주소에 따라 비교되므로 "다른" 타입 변수를 구별할 수 있도록 변경 가능한 타입으로 정의됩니다. 그러나 관례상 변경되어서는 안 됩니다.

TypeVar를 수동으로 생성할 수 있습니다:

julia> TypeVar(:V, Signed, Real)
Signed<:V<:Real

이러한 인수 중 name 기호를 제외한 모든 인수를 생략할 수 있는 편리한 버전이 있습니다.

Array{T} where T<:Integer는 다음과 같이 낮춰집니다.

let T = TypeVar(:T,Integer)
    UnionAll(T, Array{T})
end

그래서 TypeVar를 수동으로 구성하는 것은 드물게 필요합니다(실제로, 이는 피해야 합니다).

Free variables

자유 타입 변수의 개념은 타입 시스템에서 매우 중요합니다. 우리는 변수 V가 타입 T에서 자유롭다고 말하는데, 이는 T가 변수 V를 도입하는 UnionAll을 포함하지 않을 때를 의미합니다. 예를 들어, 타입 Array{Array{V} where V<:Integer}는 자유 변수가 없지만, 그 안에 있는 Array{V} 부분은 자유 변수 V를 가지고 있습니다.

자유 변수를 가진 타입은 어떤 의미에서 실제로는 타입이 아닙니다. Array{Array{T}} where T 타입을 고려해 보세요. 이는 모든 동종 배열의 배열을 참조합니다. 내부 타입인 Array{T}는 혼자서 보면 어떤 종류의 배열을 참조하는 것처럼 보일 수 있습니다. 그러나 외부 배열의 모든 요소는 같은 배열 타입을 가져야 하므로 Array{T}는 단순히 어떤 오래된 배열을 참조할 수 없습니다. Array{T}는 효과적으로 여러 번 "발생"한다고 말할 수 있으며, T는 각 "시간"마다 같은 값을 가져야 합니다.

이러한 이유로 C API의 jl_has_free_typevars 함수는 매우 중요합니다. 이 함수가 true를 반환하는 타입은 서브타입 및 기타 타입 함수에서 의미 있는 답변을 제공하지 않습니다.

TypeNames

다음 두 Array 유형은 기능적으로 동등하지만 출력이 다릅니다:

julia> TV, NV = TypeVar(:T), TypeVar(:N)
(T, N)

julia> Array
Array

julia> Array{TV, NV}
Array{T, N}

이들은 TypeName 유형의 객체인 name 필드를 검사하여 구분할 수 있습니다:

julia> dump(Array{Int,1}.name)
TypeName
  name: Symbol Array
  module: Module Core
  names: empty SimpleVector
  wrapper: UnionAll
    var: TypeVar
      name: Symbol T
      lb: Union{}
      ub: Any
    body: UnionAll
      var: TypeVar
        name: Symbol N
        lb: Union{}
        ub: Any
      body: Array{T, N} <: DenseArray{T, N}
  cache: SimpleVector
    ...

  linearcache: SimpleVector
    ...

  hash: Int64 -7900426068641098781
  mt: MethodTable
    name: Symbol Array
    defs: Nothing nothing
    cache: Nothing nothing
    max_args: Int64 0
    module: Module Core
    : Int64 0
    : Int64 0

이 경우 관련 필드는 wrapper로, 새로운 Array 유형을 만들기 위해 사용되는 최상위 유형에 대한 참조를 보유합니다.

julia> pointer_from_objref(Array)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array.body.body.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array{TV,NV})
Ptr{Cvoid} @0x00007fcc80c4d930

julia> pointer_from_objref(Array{TV,NV}.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

Arraywrapper 필드는 자신을 가리키지만, Array{TV,NV}는 타입의 원래 정의로 다시 가리킵니다.

다른 필드는 어떨까요? hash는 각 유형에 정수를 할당합니다. cache 필드를 살펴보려면 Array보다 덜 자주 사용되는 유형을 선택하는 것이 좋습니다. 먼저 우리만의 유형을 만들어 봅시다:

julia> struct MyType{T,N} end

julia> MyType{Int,2}
MyType{Int64, 2}

julia> MyType{Float32, 5}
MyType{Float32, 5}

매개변수 유형을 인스턴스화할 때, 각 구체적인 유형은 유형 캐시(MyType.body.body.name.cache)에 저장됩니다. 그러나 자유 유형 변수를 포함하는 인스턴스는 캐시되지 않습니다.

Tuple types

튜플 타입은 흥미로운 특별 사례를 구성합니다. x::Tuple과 같은 선언에서 디스패치가 작동하려면, 타입이 모든 튜플을 수용할 수 있어야 합니다. 매개변수를 확인해 봅시다:

julia> Tuple
Tuple

julia> Tuple.parameters
svec(Vararg{Any})

다른 유형과 달리, 튜플 유형은 매개변수에서 공변적이므로 이 정의는 Tuple이 모든 유형의 튜플과 일치하도록 허용합니다:

julia> typeintersect(Tuple, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{Any}}, Tuple{Int,Float64})
Tuple{Int64, Float64}

그러나 가변 인자(Vararg) 튜플 타입에 자유 변수가 있는 경우, 다양한 종류의 튜플을 설명할 수 있습니다:

julia> typeintersect(Tuple{Vararg{T} where T}, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{T}} where T, Tuple{Int,Float64})
Union{}

TTuple 타입에 대해 자유로울 때(즉, 그 바인딩 UnionAll 타입이 Tuple 타입 외부에 있을 때), 전체 타입에 대해 하나의 T 값만 작동해야 합니다. 따라서 이질적인 튜플은 일치하지 않습니다.

마지막으로, Tuple{}는 다르다는 점에 유의할 가치가 있습니다:

julia> Tuple{}
Tuple{}

julia> Tuple{}.parameters
svec()

julia> typeintersect(Tuple{}, Tuple{Int})
Union{}

"기본" 튜플 타입은 무엇인가요?

julia> pointer_from_objref(Tuple)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{})
Ptr{Cvoid} @0x00007f5998a570d0

julia> pointer_from_objref(Tuple.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{}.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

그래서 Tuple == Tuple{Vararg{Any}}는 실제로 기본 타입입니다.

Diagonal types

Tuple{T,T} where T 유형을 고려해 보세요. 이 서명이 있는 메서드는 다음과 같이 보일 것입니다:

f(x::T, y::T) where {T} = ...

일반적인 UnionAll 타입의 해석에 따르면, 이 TAny를 포함한 모든 타입을 포함하므로 이 타입은 Tuple{Any,Any}와 동등해야 합니다. 그러나 이 해석은 몇 가지 실질적인 문제를 일으킵니다.

먼저, T의 값이 메서드 정의 내에서 사용 가능해야 합니다. f(1, 1.0)과 같은 호출에서는 T가 무엇인지 명확하지 않습니다. Union{Int,Float64}일 수도 있고, 아마도 Real일 수도 있습니다. 직관적으로, 우리는 선언 x::TT === typeof(x)를 의미한다고 기대합니다. 이 불변성이 유지되도록 하려면, 이 메서드 내에서 typeof(x) === typeof(y) === T가 되어야 합니다. 이는 메서드가 정확히 같은 타입의 인수에 대해서만 호출되어야 함을 의미합니다.

두 값이 동일한 유형인지 여부에 따라 분기할 수 있는 것이 매우 유용하다는 것이 밝혀졌습니다(예를 들어, 이는 승격 시스템에서 사용됩니다). 따라서 Tuple{T,T} where T에 대한 다른 해석을 원할 여러 가지 이유가 있습니다. 이를 작동하게 하려면 서브타입에 다음 규칙을 추가합니다: 변수가 공변 위치에서 두 번 이상 발생하면 구체적인 유형만을 범위로 제한됩니다. ("공변 위치"는 변수가 발생하는 위치와 이를 도입하는 UnionAll 유형 사이에 오직 TupleUnion 유형만이 존재함을 의미합니다.) 이러한 변수를 "대각선 변수" 또는 "구체적 변수"라고 합니다.

예를 들어, Tuple{T,T} where TUnion{Tuple{Int8,Int8}, Tuple{Int16,Int16}, ...}로 볼 수 있으며, 여기서 T는 모든 구체적인 타입을 포함합니다. 이는 흥미로운 서브타입 결과를 초래합니다. 예를 들어, Tuple{Real,Real}Tuple{T,T} where T의 서브타입이 아닙니다. 왜냐하면 Tuple{Int8,Int16}와 같이 두 요소의 타입이 다른 타입을 포함하기 때문입니다. Tuple{Real,Real}Tuple{T,T} where T는 비자명한 교차점 Tuple{T,T} where T<:Real을 가지고 있습니다. 그러나 Tuple{Real}Tuple{T} where T의 서브타입입니다. 왜냐하면 이 경우 T는 한 번만 나타나므로 대각선이 아니기 때문입니다.

다음으로 다음과 같은 서명을 고려해 보십시오:

f(a::Array{T}, x::T, y::T) where {T} = ...

이 경우, TArray{T} 내부에서 불변 위치에 발생합니다. 이는 전달되는 배열의 유형이 T의 값을 명확하게 결정한다는 것을 의미합니다. 우리는 T동등성 제약을 가지고 있다고 말합니다. 따라서 이 경우 대각선 규칙은 실제로 필요하지 않으며, 배열이 T를 결정하므로 xyT의 모든 하위 유형일 수 있습니다. 따라서 불변 위치에 발생하는 변수는 결코 대각선으로 간주되지 않습니다. 이러한 동작 선택은 약간 논란의 여지가 있습니다. 일부는 이 정의가 다음과 같이 작성되어야 한다고 생각합니다.

f(a::Array{T}, x::S, y::S) where {T, S<:T} = ...

xy가 동일한 타입을 가져야 하는지 명확히 하기 위해. 이 서명 버전에서는 그렇고, xy가 서로 다른 타입을 가질 수 있다면 y의 타입을 위한 세 번째 변수를 도입할 수 있습니다.

다음 합병증은 유니온과 대각선 변수의 상호작용입니다. 예:

f(x::Union{Nothing,T}, y::T) where {T} = ...

이 선언이 의미하는 바를 고려해 보십시오. y는 타입 T를 가집니다. 그러면 x는 동일한 타입 T를 가질 수도 있고, 아니면 타입 Nothing일 수도 있습니다. 따라서 다음의 모든 호출은 일치해야 합니다:

f(1, 1)
f("", "")
f(2.0, 2.0)
f(nothing, 1)
f(nothing, "")
f(nothing, 2.0)

이 예제들은 우리에게 무언가를 말해줍니다: xnothing::Nothing일 때, y에 대한 추가 제약이 없습니다. 마치 메서드 시그니처에 y::Any가 있는 것과 같습니다. 실제로, 우리는 다음과 같은 타입 동등성을 가지고 있습니다:

(Tuple{Union{Nothing,T},T} where T) == Union{Tuple{Nothing,Any}, Tuple{T,T} where T}

일반적인 규칙은 공변 위치에 있는 구체적인 변수가 서브타이핑 알고리즘이 그것을 한 번만 사용할 경우 구체적이지 않은 것처럼 작용한다는 것입니다. x의 타입이 Nothing일 때, 우리는 Union{Nothing,T}에서 T를 사용할 필요가 없습니다; 우리는 그것을 두 번째 슬롯에서만 사용합니다. 이는 Tuple{T} where T에서 T를 구체적인 타입으로 제한하는 것이 아무런 차이를 만들지 않는다는 관찰에서 자연스럽게 발생합니다; 두 경우 모두 타입은 Tuple{Any}와 같습니다.

그러나 불변 위치에 나타나는 것은 변수가 사용되든 사용되지 않든 변수를 구체적으로 만들 수 없게 합니다. 그렇지 않으면 타입은 비교되는 다른 타입에 따라 다르게 동작할 수 있어 서브타이핑이 전이적이지 않게 됩니다. 예를 들어, 다음을 고려하십시오.

Tuple{Int,Int8,Vector{Integer}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

TUnion 내에서 무시된다면, T는 구체적이며 답은 "false"입니다. 왜냐하면 처음 두 타입이 같지 않기 때문입니다. 하지만 대신 고려해 보십시오.

Tuple{Int,Int8,Vector{Any}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

이제 우리는 Union에서 T를 무시할 수 없으므로(T == Any여야 함), T는 구체적이지 않으며 답은 "true"입니다. 이는 T의 구체성이 다른 타입에 의존하게 만들며, 이는 허용되지 않습니다. 타입은 스스로 명확한 의미를 가져야 하기 때문입니다. 따라서 Vector 내부의 T의 출현은 두 경우 모두 고려됩니다.

Subtyping diagonal variables

대각선 변수를 위한 서브타이핑 알고리즘은 두 가지 구성 요소로 이루어져 있습니다: (1) 변수 발생 식별, (2) 대각선 변수가 구체적인 타입만을 범위로 하도록 보장하기.

첫 번째 작업은 환경의 각 변수에 대해 occurs_invoccurs_cov 카운터( src/subtype.c에 있음)를 유지하여 불변 및 공변 발생의 수를 추적하는 것입니다. 변수가 대각선일 때는 occurs_inv == 0 && occurs_cov > 1입니다.

두 번째 작업은 변수의 하한에 조건을 부과함으로써 수행됩니다. 서브타입 알고리즘이 실행되는 동안 각 변수의 경계를 좁히며(하한을 높이고 상한을 낮추어) 서브타입 관계가 유지될 수 있는 변수 값의 범위를 추적합니다. 대각선 변수가 있는 UnionAll 타입의 본체 평가가 끝나면 경계의 최종 값을 살펴봅니다. 변수가 구체적이어야 하므로, 하한이 구체적 타입의 서브타입이 될 수 없다면 모순이 발생합니다. 예를 들어, AbstractArray와 같은 추상 타입은 구체적 타입의 서브타입이 될 수 없지만, Int와 같은 구체적 타입은 될 수 있으며, 빈 타입인 Bottom도 마찬가지입니다. 하한이 이 테스트를 통과하지 못하면 알고리즘은 false라는 답변으로 중단됩니다.

예를 들어, 문제 Tuple{Int,String} <: Tuple{T,T} where T에서, TUnion{Int,String}의 슈퍼타입이라면 이것이 참이 될 것이라고 유도합니다. 그러나 Union{Int,String}는 추상 타입이므로, 이 관계는 성립하지 않습니다.

이 구체성 테스트는 함수 is_leaf_bound에 의해 수행됩니다. 이 테스트는 jl_is_leaf_type와 약간 다르며, Bottom에 대해서도 true를 반환합니다. 현재 이 함수는 휴리스틱이며 모든 가능한 구체적 유형을 포착하지는 않습니다. 하한이 구체적인지 여부는 다른 유형 변수의 경계 값에 따라 달라질 수 있기 때문에 어려움이 있습니다. 예를 들어, Vector{T}T의 상한과 하한이 모두 Int일 때만 구체적 유형 Vector{Int}와 동등합니다. 우리는 아직 이를 위한 완전한 알고리즘을 개발하지 않았습니다.

Introduction to the internal machinery

대부분의 타입 관련 작업은 jltypes.csubtype.c 파일에서 찾을 수 있습니다. 시작하는 좋은 방법은 서브타이핑이 작동하는 모습을 보는 것입니다. make debug로 Julia를 빌드하고 디버거 내에서 Julia를 실행하세요. gdb debugging tips에는 유용할 수 있는 몇 가지 팁이 있습니다.

REPL 자체에서 서브타이핑 코드가 많이 사용되기 때문에 – 따라서 이 코드의 중단점이 자주 발생하므로 – 다음 정의를 만드는 것이 가장 쉽습니다:

julia> function mysubtype(a,b)
           ccall(:jl_breakpoint, Cvoid, (Any,), nothing)
           a <: b
       end

그런 다음 jl_breakpoint에서 중단점을 설정합니다. 이 중단점이 트리거되면 다른 함수에서 중단점을 설정할 수 있습니다.

워밍업으로 다음을 시도해 보세요:

mysubtype(Tuple{Int, Float64}, Tuple{Integer, Real})

우리는 더 복잡한 사례를 시도함으로써 더 흥미롭게 만들 수 있습니다:

mysubtype(Tuple{Array{Int,2}, Int8}, Tuple{Array{T}, T} where T)

Subtyping and method sorting

type_morespecific 함수는 메서드 테이블에서 함수에 대한 부분 순서를 부여하는 데 사용됩니다 (가장 구체적인 것에서 가장 덜 구체적인 것까지). 구체성은 엄격합니다; 만약 ab보다 더 구체적이라면, ab와 같지 않으며 ba보다 더 구체적이지 않습니다.

ab의 엄격한 하위 유형이라면, 자동으로 더 구체적인 것으로 간주됩니다. 거기에서 type_morespecific는 덜 공식적인 규칙을 사용합니다. 예를 들어, subtype은 인수의 수에 민감하지만, type_morespecific는 그럴 필요가 없을 수 있습니다. 특히, Tuple{Int,AbstractFloat}Tuple{Integer}보다 더 구체적이지만, 하위 유형은 아닙니다. (Tuple{Int,AbstractFloat}Tuple{Integer,Float64} 중 어느 것도 서로보다 더 구체적이지 않습니다.) 마찬가지로, Tuple{Int,Vararg{Int}}Tuple{Integer}의 하위 유형이 아니지만, 더 구체적인 것으로 간주됩니다. 그러나 morespecific는 길이에 대해 보너스를 받습니다: 특히, Tuple{Int,Int}Tuple{Int,Vararg{Int}}보다 더 구체적입니다.

메서드가 어떻게 정렬되는지 디버깅하는 경우, 함수를 정의하는 것이 편리할 수 있습니다:

type_morespecific(a, b) = ccall(:jl_type_morespecific, Cint, (Any,Any), a, b)

튜플 타입 a가 튜플 타입 b보다 더 구체적인지 테스트할 수 있게 해줍니다.