타입 관계

타입 관계

코드를 쓰다 보면 컴파일러가 "이 타입을 저 타입으로 바꿔도 되나요?" 같은 질문을 스스로 하고 있다는 걸 느낄 때가 있어요. 함수에 값을 넘기거나, 변수에 할당하거나, 형변환을 할 때마다 말이죠. 이 절은 그런 판단의 기준이 되는 타입들 사이의 관계를 네 가지로 나눠 정리해요. 같은 타입인지, 누가 누구의 하위인지, 서로 변환할 수 있는지, 할당할 수 있는지 — 이 관계들이 실제로는 컴파일러가 타입 검사를 수행할 때 규칙으로 작동해요.

출처: Nim Manual

본문

원문이 구조적 타입 동등성부터 시작하는 흐름을 그대로 따라가 볼게요. 차례대로 짚다 보면 각 관계가 어디에서 다른 관계와 맞물리는지가 자연스럽게 보여요.

타입 동등성 (Type equality)

Nim은 대부분의 타입에 대해 구조적 타입 동등성(structural type equivalence)을 사용해요. 구조가 같으면 서로 같은 타입으로 보는 거죠. 다만 예외가 있는데, 객체(object), 열거형(enumeration), distinct 타입, 그리고 제네릭 타입은 이름 동등성(name equivalence)을 따릅니다. 같은 구조를 갖고 있더라도 이름이 다르면 서로 다른 타입으로 취급한다는 뜻이에요.

하위 타입 관계 (Subtype relation)

객체 a가 객체 b를 상속한다면, ab의 하위 타입(subtype)이 돼요.

이 하위 타입 관계는 그대로 var, ref, ptr 타입으로도 확장됩니다. AB의 하위 타입이고 A, B가 둘 다 object 타입이라면 다음이 성립해요:

  • var Avar B 의 하위 타입

  • ref Aref B 의 하위 타입

  • ptr Aptr B 의 하위 타입.

참고: 하위 타입에서 부모 타입으로 할당할 때는 위의 포인터 간접 참조 가운데 하나가 반드시 필요해요. 그래야 "객체 슬라이싱"(object slicing)을 막을 수 있거든요. 값으로 그대로 할당하면 자식 타입이 추가로 갖고 있던 필드가 잘려나가버리니까요.

변환 가능 관계 (Convertible relation)

타입 a가 타입 b암시적으로 변환 가능한지(implicitly convertible)는 아래 알고리즘이 true를 돌려주는지에 달려 있어요:

proc isImplicitlyConvertible(a, b: PType): bool =
  if isSubtype(a, b):
    return true
  if isIntLiteral(a):
    return b in {int8, int16, int32, int64, int, uint, uint8, uint16,
                 uint32, uint64, float32, float64}
  case a.kind
  of int:     result = b in {int32, int64}
  of int8:    result = b in {int16, int32, int64, int}
  of int16:   result = b in {int32, int64, int}
  of int32:   result = b in {int64, int}
  of uint:    result = b in {uint32, uint64}
  of uint8:   result = b in {uint16, uint32, uint64}
  of uint16:  result = b in {uint32, uint64}
  of uint32:  result = b in {uint64}
  of float32: result = b in {float64}
  of float64: result = b in {float32}
  of seq:
    result = b == openArray and typeEquals(a.baseType, b.baseType)
  of array:
    result = b == openArray and typeEquals(a.baseType, b.baseType)
    if a.baseType == char and a.indexType.rangeA == 0:
      result = b == cstring
  of cstring, ptr:
    result = b == pointer
  of string:
    result = b == cstring
  of proc:
    result = typeEquals(a, b) or compatibleParametersAndEffects(a, b)

위 코드에서 "타입 동등성"을 나타내는 술어로 typeEquals(a, b)를, "하위 타입 관계"를 나타내는 술어로 isSubtype(a, b)를 썼어요. compatibleParametersAndEffects(a, b)는 현재 구체적으로 정의되지 않은 상태예요.

Nim의 range 타입 생성자에도 암시적 변환이 적용돼요. a0, b0가 타입 T일 때, A = range[a0..b0]가 인자의 타입이고 F가 형식 매개변수의 타입이라고 해볼게요. 그러면 A에서 F로의 암시적 변환은 a0 >= low(F) and b0 <= high(F)이면서 TF가 둘 다 부호 있는 정수이거나, 둘 다 부호 없는 정수일 때 존재해요.

한편 타입 a가 타입 b명시적으로 변환 가능한지(explicitly convertible)는 다음 알고리즘으로 판단합니다:

proc isIntegralType(t: PType): bool =
  result = isOrdinal(t) or t.kind in {float, float32, float64}

proc isExplicitlyConvertible(a, b: PType): bool =
  result = false
  if isImplicitlyConvertible(a, b): return true
  if typeEquals(a, b): return true
  if a == distinct and typeEquals(a.baseType, b): return true
  if b == distinct and typeEquals(b.baseType, a): return true
  if isIntegralType(a) and isIntegralType(b): return true
  if isSubtype(a, b) or isSubtype(b, a): return true

변환 가능 관계는 사용자가 직접 정의한 타입 변환자(converter)로 완화할 수도 있어요. 아래 예처럼 charint로 바꿔주는 변환자를 하나 만들어 두면, 변환 코드를 일일이 쓰지 않아도 자동으로 적용됩니다:

converter toInt(x: char): int = result = ord(x)

var
  x: int
  chr: char = 'a'

# implicit conversion magic happens here
x = chr
echo x # => 97
# one can use the explicit form too
x = chr.toInt
echo x # => 97

T(a) 형태의 타입 변환은, a가 l-value이고 typeEqualsOrDistinct(T, typeof(a))가 성립할 때 그 자체로도 l-value가 돼요.

할당 호환성 (Assignment compatibility)

표현식 b를 표현식 a에 할당할 수 있으려면, al-value이고 isImplicitlyConvertible(b.typ, a.typ)가 성립해야 해요. 정리하면 할당은 '받는 쪽이 실제 저장 위치(l-value)인지'와 '넣는 쪽의 타입이 받는 쪽 타입으로 암시적으로 변환되는지'가 관건인 셈이에요.

더 알아보기 (Learn more)

  • 이 절의 원문을 함께 보면 좋아요: Type relations
  • 타입 자체의 개념과 구분은 매뉴얼의 Types 절을 참고하세요.
  • 변환자를 더 깊이 다루는 내용은 Converters 절에서 계속 이어져요.