Conversion and Promotion
줄리아는 수학 연산자의 인수를 공통 유형으로 승격시키는 시스템을 가지고 있으며, 이는 Integers and Floating-Point Numbers, Mathematical Operations and Elementary Functions, Types, 및 Methods를 포함한 여러 섹션에서 언급되었습니다. 이 섹션에서는 이 승격 시스템이 어떻게 작동하는지, 새로운 유형으로 확장하는 방법, 그리고 내장 수학 연산자 외의 함수에 적용하는 방법을 설명합니다. 전통적으로 프로그래밍 언어는 산술 인수의 승격에 대해 두 가지 진영으로 나뉩니다:
- 내장 산술 유형 및 연산자에 대한 자동 승격. 대부분의 언어에서,
+
,-
,*
,/
와 같은 중위 구문을 가진 산술 연산자의 피연산자로 사용될 때, 내장 숫자 유형은 일반적으로 예상되는 결과를 생성하기 위해 공통 유형으로 자동 승격됩니다. C, Java, Perl, Python 등은 모두1 + 1.5
의 합을 부동 소수점 값2.5
로 올바르게 계산합니다. 이는+
의 피연산자 중 하나가 정수임에도 불구하고 발생합니다. 이러한 시스템은 편리하며 프로그래머에게 거의 보이지 않도록 충분히 신중하게 설계되었습니다. 이러한 표현식을 작성할 때 이 승격이 발생한다고 의식적으로 생각하는 사람은 거의 없지만, 컴파일러와 인터프리터는 정수와 부동 소수점 값을 그대로 더할 수 없기 때문에 덧셈 전에 변환을 수행해야 합니다. 따라서 이러한 자동 변환을 위한 복잡한 규칙은 그러한 언어의 사양 및 구현의 필수적인 부분이 됩니다. - 자동 승격 없음. 이 캠프는 Ada와 ML을 포함합니다. 매우 "엄격한" 정적 타입 언어입니다. 이러한 언어에서는 모든 변환이 프로그래머에 의해 명시적으로 지정되어야 합니다. 따라서 예제 표현식
1 + 1.5
는 Ada와 ML 모두에서 컴파일 오류가 발생합니다. 대신real(1) + 1.5
와 같이 정수1
을 부동 소수점 값으로 명시적으로 변환한 후 덧셈을 수행해야 합니다. 그러나 모든 곳에서 명시적 변환을 하는 것은 매우 불편하기 때문에 Ada조차도 어느 정도의 자동 변환을 지원합니다: 정수 리터럴은 예상되는 정수 타입으로 자동으로 승격되고, 부동 소수점 리터럴도 적절한 부동 소수점 타입으로 유사하게 승격됩니다.
어떤 의미에서, Julia는 "자동 승격 없음" 범주에 속합니다: 수학 연산자는 특별한 구문을 가진 함수일 뿐이며, 함수의 인수는 절대 자동으로 변환되지 않습니다. 그러나 다양한 혼합 인수 유형에 수학 연산을 적용하는 것은 다형성 다중 분배의 극단적인 경우에 불과하다는 것을 관찰할 수 있습니다. 이는 Julia의 분배 및 유형 시스템이 특히 잘 처리할 수 있는 것입니다. 수학 피연산자의 "자동" 승격은 단순히 특별한 응용으로 나타납니다: Julia는 특정 피연산자 유형 조합에 대한 구체적인 구현이 존재하지 않을 때 호출되는 수학 연산자에 대한 미리 정의된 포괄적 분배 규칙을 제공합니다. 이러한 포괄적 규칙은 먼저 모든 피연산자를 사용자 정의 승격 규칙을 사용하여 공통 유형으로 승격시키고, 그 다음 결과 값에 대해 이제 동일한 유형의 연산자에 대한 전문화된 구현을 호출합니다. 사용자 정의 유형은 다른 유형으로의 변환을 위한 메서드를 정의하고, 다른 유형과 혼합될 때 승격해야 할 유형을 정의하는 몇 가지 승격 규칙을 제공함으로써 이 승격 시스템에 쉽게 참여할 수 있습니다.
Conversion
특정 유형 T
의 값을 얻는 표준 방법은 유형의 생성자 T(x)
를 호출하는 것입니다. 그러나 프로그래머가 명시적으로 요청하지 않고도 한 유형의 값을 다른 유형으로 변환하는 것이 편리한 경우가 있습니다. 한 예로, 값을 배열에 할당하는 경우가 있습니다: 만약 A
가 Vector{Float64}
라면, 표현식 A[1] = 2
는 2
를 Int
에서 Float64
로 자동으로 변환하고 결과를 배열에 저장함으로써 작동해야 합니다. 이는 convert
함수를 통해 수행됩니다.
convert
함수는 일반적으로 두 개의 인수를 받습니다: 첫 번째는 타입 객체이고 두 번째는 해당 타입으로 변환할 값입니다. 반환된 값은 주어진 타입의 인스턴스로 변환된 값입니다. 이 함수를 이해하는 가장 간단한 방법은 실제로 사용하는 것을 보는 것입니다:
julia> x = 12
12
julia> typeof(x)
Int64
julia> xu = convert(UInt8, x)
0x0c
julia> typeof(xu)
UInt8
julia> xf = convert(AbstractFloat, x)
12.0
julia> typeof(xf)
Float64
julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
1 2 3
4 5 6
julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
1.0 2.0 3.0
4.0 5.0 6.0
변환이 항상 가능한 것은 아니며, 이 경우 MethodError
가 발생하여 convert
가 요청된 변환을 수행하는 방법을 모른다는 것을 나타냅니다:
julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]
일부 언어는 문자열을 숫자로 파싱하거나 숫자를 문자열로 포맷하는 것을 변환으로 간주합니다(많은 동적 언어는 자동으로 변환을 수행하기도 합니다). 그러나 Julia에서는 그렇지 않습니다. 일부 문자열은 숫자로 파싱될 수 있지만, 대부분의 문자열은 유효한 숫자 표현이 아니며, 그 중에서도 매우 제한된 하위 집합만이 유효합니다. 따라서 Julia에서는 이 작업을 수행하기 위해 전용 parse
함수를 사용해야 하며, 이는 더 명시적입니다.
When is convert
called?
다음 언어 구성 요소는 convert
를 호출합니다:
- 배열에 할당하면 배열의 요소 유형으로 변환됩니다.
- 객체의 필드에 할당하면 필드의 선언된 타입으로 변환됩니다.
new
로 객체를 구성하면 객체의 선언된 필드 유형으로 변환됩니다.- 변수에 선언된 타입으로 할당하기 (예:
local x::T
)는 해당 타입으로 변환됩니다. - 선언된 반환 유형이 있는 함수는 반환 값을 해당 유형으로 변환합니다.
ccall
에 값을 전달하면 해당 인수 유형으로 변환됩니다.
Conversion vs. Construction
convert(T, x)
의 동작은 T(x)
와 거의 동일해 보입니다. 실제로 보통 그렇습니다. 그러나 중요한 의미상의 차이가 있습니다. convert
는 암시적으로 호출될 수 있기 때문에, 그 메서드는 "안전한" 또는 "예상치 않은" 것으로 간주되는 경우로 제한됩니다. convert
는 동일한 기본 종류의 것을 나타내는 타입 간에만 변환합니다(예: 숫자의 다양한 표현 또는 문자열 인코딩의 차이). 또한 일반적으로 손실이 없습니다; 값을 다른 타입으로 변환한 후 다시 변환하면 정확히 동일한 값이 나와야 합니다.
생성자가 convert
와 다른 네 가지 일반적인 경우가 있습니다:
Constructors for types unrelated to their arguments
일부 생성자는 "변환" 개념을 구현하지 않습니다. 예를 들어, Timer(2)
는 2초 타이머를 생성하는데, 이는 정수에서 타이머로의 "변환"이라고 볼 수 없습니다.
Mutable collections
convert(T, x)
는 x
가 이미 T
유형인 경우 원래의 x
를 반환해야 합니다. 반면, T
가 가변 컬렉션 유형인 경우 T(x)
는 항상 x
에서 요소를 복사하여 새 컬렉션을 만들어야 합니다.
Wrapper types
일부 값들을 "감싸는" 유형의 경우, 생성자는 요청된 유형이 이미 있는 경우에도 인수를 새 객체 안에 감쌀 수 있습니다. 예를 들어 Some(x)
는 값이 존재함을 나타내기 위해 x
를 감쌉니다(결과가 Some
또는 nothing
일 수 있는 맥락에서). 그러나 x
자체가 객체 Some(y)
일 수 있으며, 이 경우 결과는 두 개의 레벨로 감싸진 Some(Some(y))
입니다. 반면에 convert(Some, x)
는 이미 Some
이므로 단순히 x
를 반환합니다.
Constructors that don't return instances of their own type
매우 드문 경우에 생성자 T(x)
가 T
유형이 아닌 객체를 반환하는 것이 의미가 있을 수 있습니다. 이는 래퍼 유형이 자신의 역수일 때(예: Flip(Flip(x)) === x
) 발생할 수 있으며, 라이브러리가 재구성될 때 이전 호출 구문을 지원하기 위해 하위 호환성을 유지하기 위해서일 수 있습니다. 그러나 convert(T, x)
는 항상 T
유형의 값을 반환해야 합니다.
Defining New Conversions
새로운 유형을 정의할 때, 처음에는 생성자로서 생성하는 모든 방법을 정의해야 합니다. 암시적 변환이 유용할 것이라는 것이 분명해지고, 일부 생성자가 위의 "안전성" 기준을 충족하는 경우, convert
메서드를 추가할 수 있습니다. 이러한 메서드는 일반적으로 적절한 생성자를 호출하기만 하면 되므로 매우 간단합니다. 이러한 정의는 다음과 같이 보일 수 있습니다:
import Base: convert
convert(::Type{MyType}, x) = MyType(x)
이 메서드의 첫 번째 인수의 유형은 Type{MyType}
이며, 이 인스턴스는 MyType
뿐입니다. 따라서 이 메서드는 첫 번째 인수가 유형 값 MyType
일 때만 호출됩니다. 첫 번째 인수에 사용된 구문에 주목하십시오: ::
기호 이전에 인수 이름이 생략되고 유형만 제공됩니다. 이것은 값이 이름으로 참조될 필요는 없지만 유형이 지정된 함수 인수에 대한 Julia의 구문입니다.
모든 추상 타입의 인스턴스는 기본적으로 "충분히 유사하다"고 간주되며, Julia Base에서는 보편적인 convert
정의가 제공됩니다. 예를 들어, 이 정의는 1-인자 생성자를 호출하여 어떤 Number
타입을 다른 타입으로 convert
하는 것이 유효하다고 명시합니다:
convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T
이것은 새로운 Number
유형이 생성자만 정의하면 된다는 것을 의미합니다. 이 정의가 convert
를 처리할 것이기 때문입니다. 인수가 이미 요청된 유형인 경우를 처리하기 위해 항등 변환도 제공됩니다:
convert(::Type{T}, x::T) where {T<:Number} = x
유사한 정의는 AbstractString
, AbstractArray
, 및 AbstractDict
에 대해 존재합니다.
Promotion
프로모션은 혼합 유형의 값을 단일 공통 유형으로 변환하는 것을 의미합니다. 엄격히 필요하지는 않지만, 일반적으로 값이 변환되는 공통 유형은 원래의 모든 값을 충실히 표현할 수 있어야 한다고 암시됩니다. 이러한 의미에서 "프로모션"이라는 용어는 적절합니다. 왜냐하면 값이 "더 큰" 유형으로 변환되기 때문입니다. 즉, 모든 입력 값을 단일 공통 유형으로 표현할 수 있는 유형입니다. 그러나 이것을 객체 지향(구조적) 슈퍼타입이나 줄리아의 추상 슈퍼타입 개념과 혼동하지 않는 것이 중요합니다. 프로모션은 유형 계층과는 아무런 관련이 없으며, 대체 표현 간의 변환과 관련이 있습니다. 예를 들어, 모든 Int32
값은 Float64
값으로도 표현될 수 있지만, Int32
는 Float64
의 하위 유형이 아닙니다.
Julia에서 공통 "더 큰" 유형으로의 승격은 promote
함수를 통해 수행됩니다. 이 함수는 임의의 수의 인수를 받아들이고, 공통 유형으로 변환된 동일한 수의 값의 튜플을 반환하거나 승격이 불가능한 경우 예외를 발생시킵니다. 승격의 가장 일반적인 사용 사례는 숫자 인수를 공통 유형으로 변환하는 것입니다:
julia> promote(1, 2.5)
(1.0, 2.5)
julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)
julia> promote(2, 3//4)
(2//1, 3//4)
julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)
julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)
julia> promote(1 + 2im, 3//4)
(1//1 + 2//1*im, 3//4 + 0//1*im)
부동 소수점 값은 부동 소수점 인수 유형 중 가장 큰 것으로 승격됩니다. 정수 값은 정수 인수 유형 중 가장 큰 것으로 승격됩니다. 유형의 크기가 같지만 부호가 다를 경우, 부호 없는 유형이 선택됩니다. 정수와 부동 소수점 값의 혼합은 모든 값을 담을 수 있는 충분한 크기의 부동 소수점 유형으로 승격됩니다. 정수와 유리수가 혼합되면 유리수로 승격됩니다. 유리수와 부동 소수점이 혼합되면 부동 소수점으로 승격됩니다. 복소수와 실수가 혼합되면 적절한 종류의 복소수로 승격됩니다.
프로모션을 사용하는 데는 정말로 이게 전부입니다. 나머지는 영리한 적용의 문제이며, 가장 전형적인 "영리한" 적용은 산술 연산자 +
, -
, *
및 /
와 같은 숫자 연산을 위한 포괄적인 메서드 정의입니다. 다음은 promotion.jl
에 제공된 몇 가지 포괄적인 메서드 정의입니다:
+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)
이러한 메서드 정의는 숫자 값 쌍을 더하고, 빼고, 곱하고, 나누는 데 더 구체적인 규칙이 없을 경우, 값을 공통 유형으로 승격한 다음 다시 시도하라고 말합니다. 그게 전부입니다: 산술 연산을 위한 공통 숫자 유형으로의 승격에 대해 걱정할 필요가 있는 곳은 없습니다 – 그것은 자동으로 발생합니다. promotion.jl
의 여러 다른 산술 및 수학 함수에 대한 포괄적인 승격 메서드 정의가 있지만, 그 외에는 Julia Base에서 promote
를 호출할 필요가 거의 없습니다. promote
의 가장 일반적인 사용은 편의를 위해 제공되는 외부 생성자 메서드에서 발생하며, 혼합 유형의 생성자 호출이 적절한 공통 유형으로 승격된 필드를 가진 내부 유형에 위임할 수 있도록 합니다. 예를 들어, rational.jl
는 다음과 같은 외부 생성자 메서드를 제공합니다:
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
이것은 다음과 같은 호출이 작동하도록 허용합니다:
julia> x = Rational(Int8(15),Int32(-5))
-3//1
julia> typeof(x)
Rational{Int32}
대부분의 사용자 정의 유형에 대해 프로그래머가 생성자 함수에 예상되는 유형을 명시적으로 제공하도록 요구하는 것이 더 나은 관행이지만, 때때로 특히 수치 문제의 경우 자동으로 승격하는 것이 편리할 수 있습니다.
Defining Promotion Rules
비록 원칙적으로 promote
함수에 대한 메서드를 직접 정의할 수 있지만, 이는 모든 가능한 인수 유형의 순열에 대해 많은 중복 정의가 필요합니다. 대신, promote
의 동작은 promote_rule
라는 보조 함수의 관점에서 정의됩니다. 이 함수에 대해 메서드를 제공할 수 있습니다. promote_rule
함수는 두 개의 유형 객체 쌍을 받아 다른 유형 객체를 반환하며, 인수 유형의 인스턴스는 반환된 유형으로 승격됩니다. 따라서 규칙을 정의함으로써:
import Base: promote_rule
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64
하나의 선언은 64비트와 32비트 부동 소수점 값이 함께 승격될 때, 64비트 부동 소수점으로 승격되어야 한다는 것이다. 승격 유형은 인수 유형 중 하나일 필요는 없다. 예를 들어, 다음의 승격 규칙은 모두 Julia Base에서 발생한다:
promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt
후자의 경우, 결과 유형은 BigInt
입니다. 이는 BigInt
가 임의 정밀도 정수 산술을 위해 정수를 저장할 수 있는 유일한 충분히 큰 유형이기 때문입니다. 또한 promote_rule(::Type{A}, ::Type{B})
와 promote_rule(::Type{B}, ::Type{A})
를 모두 정의할 필요는 없다는 점에 유의해야 합니다. 대칭성은 promote_rule
이 승격 과정에서 사용되는 방식에 의해 암시됩니다.
promote_rule
함수는 promote_type
라는 두 번째 함수를 정의하는 빌딩 블록으로 사용됩니다. 이 함수는 어떤 수의 타입 객체를 주면, 해당 값들이 promote
의 인수로서 승격되어야 할 공통 타입을 반환합니다. 따라서 실제 값이 없을 때, 특정 타입의 값 모음이 어떤 타입으로 승격될지를 알고 싶다면 promote_type
을 사용할 수 있습니다.
julia> promote_type(Int8, Int64)
Int64
promote_type
를 직접 오버로드하지 않는다는 점에 유의하세요: 대신 promote_rule
을 오버로드합니다. promote_type
은 promote_rule
을 사용하고 대칭성을 추가합니다. 이를 직접 오버로드하면 모호성 오류가 발생할 수 있습니다. 우리는 promote_rule
을 오버로드하여 사물이 어떻게 승격되어야 하는지를 정의하고, promote_type
을 사용하여 이를 조회합니다.
내부적으로, promote_type
는 promote
내에서 어떤 유형의 인수 값이 승격을 위해 변환되어야 하는지를 결정하는 데 사용됩니다. 호기심 많은 독자는 promotion.jl
의 코드를 읽어볼 수 있으며, 이는 약 35줄에 걸쳐 완전한 승격 메커니즘을 정의합니다.
Case Study: Rational Promotions
마침내, 우리는 줄리아의 유리수 유형에 대한 진행 중인 사례 연구를 마무리합니다. 이 유형은 다음과 같은 프로모션 규칙을 사용하여 비교적 정교하게 프로모션 메커니즘을 활용합니다:
import Base: promote_rule
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)
첫 번째 규칙은 유리수를 다른 정수 유형으로 승격할 때, 분자/분모 유형이 다른 정수 유형과의 승격 결과인 유리 유형으로 승격된다고 말합니다. 두 번째 규칙은 서로 다른 두 유형의 유리수에 동일한 논리를 적용하여, 각 분자/분모 유형의 승격 결과인 유리수를 생성합니다. 세 번째이자 마지막 규칙은 유리수를 부동 소수점으로 승격할 때, 분자/분모 유형을 부동 소수점으로 승격한 것과 동일한 유형이 된다고 규정합니다.
이 소수의 승격 규칙과 타입의 생성자, 숫자에 대한 기본 convert
메서드는 유리수가 Julia의 다른 모든 숫자 타입 – 정수, 부동 소수점 숫자, 복소수와 완전히 자연스럽게 상호 작용할 수 있도록 충분합니다. 적절한 변환 메서드와 승격 규칙을 같은 방식으로 제공함으로써, 사용자 정의 숫자 타입도 Julia의 미리 정의된 숫자와 마찬가지로 자연스럽게 상호 작용할 수 있습니다.