타입

타입 에서 Julia

1개의 연습 문제

타입 소개

지금까지 교육 과정에서는 타입에 대해 많이 다루지 않았지만, Julia에는 분명히 타입이 있어요:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

타입을 지정한 적이 없는데도 Julia는 알아서 타입을 할당했어요.

  1. Julia에는 설계의 중심이 되는 types가 있어요.
  2. Julia는 보통 Type Inference를 이용해 타입을 "추측"할 수 있어요.

JIT 컴파일러는 코드를 (전부) 훑어보고, 변수가 어떻게 쓰이는지 살펴본 다음, 그 쓰임에 맞는 적절한 기본 타입을 _추론_해요.

타입 승격

이전 개념들에서는 숫자 타입을 하나로 통일하는 예를 여러 번 봤어요. 가장 단순한 산술 연산부터 그랬죠.

julia> 2 + 1.3
3.3

정수(Int64)와 실수(Float64)를 더했더니 결과는 Float64가 나왔어요. 마찬가지로:

julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)

julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}

julia> [nums...]
3-element Vector{Float64}:
 3.0
 4.1
 0.25

위의 경우 튜플은 각 원소의 타입을 그대로 유지했지만, 벡터로 변환하니 모두 똑같이 Float64가 됐어요.

Julia 컴파일러는 어떤 타입 변환이 가능한지 알고 있어요. 정수에서 실수로 가는 건 문제가 없지만, 실수에서 정수로 가면 정밀도를 잃기 때문에 InexactError가 발생해요. 타입 승격은 표현식 안의 모든 값을 하나의 공통 타입으로 변환해요. 그 타입은 모든 입력과 호환될 만큼 융통성이 있죠.

같은 변환은 promote() 함수로 명시적으로 할 수도 있어요:

julia> promote(nums...)
(3.0, 4.1, 0.25)

타입 할당

간단한 튜토리얼 연습 문제를 풀 때는 타입 추론과 타입 승격에 기대도 괜찮지만, 프로그램이 커지면 더 정밀한 제어가 필요할 거예요.

대부분의 최신 프로세서에서 정수의 기본 타입은 Int64예요. Numbers 개념에서 값이 기본이 아닌 특정 타입으로 변환될 수 있다는 걸 봤어요.

julia> x = Int16(42)
42

julia> typeof(x)
Int16

하지만 변수 x에는 여전히 다른 타입의 값을 다시 할당할 수 있어요:

julia> x = "changed"
"changed"

julia> typeof(x)
String

이것이 type instability예요. 그 특징은 다음과 같아요:

  • 작은 스크립트에서는 아주 편리해요.
  • 큰 프로그램에서는 성능과 안정성에 좋지 않아요.

대신 :: 연산자를 써서 x의 타입을 지정할 수 있어요:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.

이제 y는 Int16 타입이고, 앞으로도 계속 Int16이에요. 따라서 컴파일러는 y를 위해 몇 바이트를 예약해야 하는지 알고, 안정적인 타입을 전제로 나머지 코드를 최적화할 수 있어요. 이 점에서 이런 변수는 C 같은 정적 타입 언어의 변수와 거의 비슷해요.

타입 단언

위에서 설명한 타입 할당은 변수 할당의 왼쪽에서 그 변수의 타입을 제한할 때 사용해요.

:: 연산자를 값이나 값으로 평가되는 것에 사용하는 것은, 그 값이 반드시 이 타입이어야 한다는 _단언_이에요. 그렇지 않으면 오류가 발생해야 하죠.

julia> 42::Number
42

julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String

흔히 함수의 반환값에 붙여서, 함수가 예상대로 동작했는지 확인하는 간단한 마지막 점검으로 쓸 수 있어요.

다른 형태의 단언을 위한 @assert 매크로도 있다는 점을 기억해 두세요.

타입 계층 구조

Int64, Int16, String, Char. 이 타입들은 과연 어디에서 "왔을까요".

많은 객체 지향(OO) 언어에서는 각 타입이 클래스이고, 상속으로 클래스 계층을 이루며, 클래스 메서드가 동작을 정의해요.

Java와 Ruby가 이런 방식의 대표적인 예이고, Python도 내부적으로는 비슷해요.

Julia에는 클래스가 없어요.

문서에 나온 이유는 OO 기능이 JIT 컴파일러를 방해하고 런타임 성능을 떨어뜨리기 때문이에요.

어쩌면 이 인용문도 읽었는지 몰라요:

"객체 지향 프로그래밍은 캘리포니아에서만 나올 수 있었던, 유별나게 나쁜 발상이에요."

이 말은 Edsger Dijkstra의 것으로 알려져 있어요. 1950년대부터 수십 년 동안 뛰어난 컴퓨터 과학자였지만, 밝은 낙관주의나 섬세한 외교술로 유명한 사람은 아니었어요.

그런데 이 코드를 보세요:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> supertype(Int16)
Signed

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

몇 가지를 더 자세히 살펴보면:

  • Int16은 타입이고, 이 타입의 변수를 만들 수 있어요.
  • Int16은 Signed의 하위 타입이고, supertype() 함수로 이를 확인할 수 있어요.
  • Integer, Real, Number를 거쳐 맨 위의 Any까지 이어지는 타입 계층이 있고, subtypes()가 이 계층의 해당 가지를 보여줘요.

모든 가지는 Any에서 끝나는데, Any는 자기 자신이 자기 상위 타입이라는 점에서 독특해요.

julia> supertypes(String)
(String, AbstractString, Any)

julia> supertype(Any)
Any

즉, Julia에는 class 계층은 없지만, type 계층은 분명히 있어요. 전체 계층을 다 그려 보면 거대한 트리가 나오는데, 사실상 다 볼 수가 없어요. 계층의 일부를 들여다보는 방법은 온라인에서 논의된 바 있어요.

언젠가 이게 어떻게 동작하는지 하나씩 풀어볼 거지만, 그 전에 먼저 살펴볼 것이 훨씬 많아요.

타입 검사하기

평소처럼 typeof()로 같은지 검사할 수 있어요.

julia> typeof(11)
Int64

julia> typeof(11) == Int64
true

julia> typeof(11) == Number
false

이런 비교 방식은 타입 계층을 전혀 고려하지 않기 때문에, 타입이 정확히 같아야 해요.

더 유연하게는, isa가 값의 타입이 비교 대상과 같거나 그 하위 타입인지 알려줘요. 중위 연산자 형태로도, 함수 형태로도 쓸 수 있어요.

julia> 12 isa Int64
true

julia> 12 isa Number
true

julia> isa(12, Number)
true

julia> 12 isa String
false

isa는 왼쪽에 type이 아니라 value를 기대한다는 점에 주의하세요.

이렇게 두 _타입_을 비교하려고 하면 예상치 못한 결과가 나와요. 올바른 연산자는 <:이고, 앞으로 나올 개념에서 훨씬 자주 보게 될 거예요.

julia> Int64 isa Number  ## Don't do this!
false

julia> Int64 <: Number
true

타입 검사는 함수 안에서 흐름을 제어하는 데 쓸 수 있지만, 관용적인 Julia 코드에서는 비교적 드물어요.

Multiple Dispatch 개념에서 살펴보겠지만, 함수 인자에 타입을 붙이고 Julia의 디스패치 메커니즘이 그런 로직을 처리하게 하는 편이 종종 더 효율적이에요. 하지만 Multiple Dispatch에 이르기 전에, 타입과 관련해 더 논의해야 할 개념이 여러 개 있어요.

추상 타입과 구체 타입

타입 계층이 트리 구조를 이룬다는 걸 봤어요. 컴퓨터 과학에서 말하는 트리로, 뿌리가 맨 위에 있는 구조예요.

트리의 각 항목은 node이고, 이들은 몇 가지로 나눌 수 있어요:

  1. 하위 타입이 있는 노드를 abstract라고 해요.
  2. 하위 타입이 없는 리프 노드를 concrete라고 해요.
julia> subtypes(Integer)  # an abstract type
3-element Vector{Any}:
 Bool
 Signed
 Unsigned

julia> subtypes(Int64)  # a concrete type
Type[]

구체 타입만 변수로 instantiated될 수 있기 때문에, 이 구분은 중요해요.

julia> a::Int16 = 42
42

julia> typeof(a)
Int16

julia> b::Integer = 42
42

julia> typeof(b)
Int64

추상 타입을 사용하려고 해도 (이 경우에는) 오류 메시지가 나오지 않고, 컴파일러가 알맞은 구체 타입, 즉 Integer 대신 Int64를 만들어요. 적어도 변수에 정수가 아닌 값이 할당되는 건 막아주죠:

julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.

따라서 추상 타입을 사용한 타입 할당은 변수 타입에 대한 _제약_이에요. 위의 예에서는 Integer의 하위 타입이면 무엇이든 허용돼요. 이건 프로그래밍 언어 세계에서는 드문 일이에요. C의 타입 할당보다는 약하고, 최근 Python의 타입 힌트보다는 강해요.

isabstracttype()과 isconcretetype() 함수로 검사할 수 있어요. 이 둘이 단순히 서로의 부정이 아니라는 점에 주의하세요. 나중에 나올 개념에서 어떤 타입은 추상도 구체도 아닐 수 있다는 걸 보게 될 거예요.

julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)

julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)

# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)

Vector는 구체 타입이 아니지만, 그 원소들은 구체적이에요. eltype()(원소 타입) 함수가 이 타입을 추출해요:

julia> eltype([1, 2.3])
Float64
GitHub에서 편집 링크가 새 창이나 탭에서 열려요

타입 배우기