특수 타입

특수 타입 (Special Types)

Nim의 타입 시스템에는 값을 담는 평범한 타입 말고도 조금 특별한 친구들이 몇 개 있어요. 크게 세 가지를 기억하면 되는데, 컴파일 타임 상수만 받는 static[T], 타입 자체를 값처럼 다루는 typedesc[T], 그리고 표현식의 타입을 그 자리에서 알아내는 typeof예요. 이들은 제네릭과 매크로를 쓰다 보면 자연스럽게 마주치게 되어요.

출처: https://nim-lang.org/docs/manual.html#special-types

본문

static[T]

이름이 말해 주듯 static 파라미터는 항상 **상수 표현식(constant expression)**이어야 해요:

proc precompiledRegex(pattern: static string): RegEx =
  var res {.global.} = re(pattern)
  return res

precompiledRegex("/d+") # Replaces the call with a precompiled
                        # regex, stored in a global variable

precompiledRegex(paramStr(1)) # Error, command-line options
                              # are not constant expressions

정규식이 정확히 한 번만 컴파일돼 전역 변수에 저장되는 걸 눈여겨보세요. paramStr(1)처럼 호출 시점에만 알 수 있는 값은 상수가 아니므로 컴파일 오류가 나요.

코드 생성 관점에서 보면 모든 static 파라미터는 제네릭 파라미터로 취급돼요. 즉 프로시저는 공급되는 각각의 고유한 값(또는 값의 조합)마다 따로 컴파일돼요.

static 파라미터는 제네릭 타입의 시그니처에도 등장할 수 있어요:

type
  Matrix[M,N: static int; T: Number] = array[0..(M*N - 1), T]
    # Note how `Number` is just a type constraint here, while
    # `static int` requires us to supply an int value

  AffineTransform2D[T] = Matrix[3, 3, T]
  AffineTransform3D[T] = Matrix[4, 4, T]

var m1: AffineTransform3D[float]  # OK
var m2: AffineTransform2D[string] # Error, `string` is not a `Number`

Number는 단지 타입 제약이고, static int정수 값을 요구한다는 차이를 코드 주석이 잘 짚어 주네요.

static T는 사실 밑바탕의 제네릭 타입 static[T]를 쓰기 편하게 만든 문법 설탕(syntactic sugar)이에요. 타입 파라미터를 생략하면 "모든 상수 표현식"이라는 타입 클래스를 얻고, static을 다른 타입 클래스로 인스턴스화하면 더 구체적인 타입 클래스를 만들 수 있어요.

표현식을 대응하는 static 타입으로 강제(코어스)하면 컴파일 타임에 상수로 평가되도록 만들 수 있어요:

import std/math

echo static(fac(5)), " ", static[bool](16.isPowerOfTwo)

표현식을 평가하지 못하거나 타입이 어긋나면 컴파일러가 오류를 보고해요.

typedesc[T]

Nim은 많은 문맥에서 타입의 이름을 평범한 값처럼 취급해요. 이런 값들은 컴파일 단계에서만 존재하지만, 모든 값에는 타입이 있어야 하므로 typedesc가 그 값들을 위한 특별한 타입으로 간주돼요.

typedesc는 제네릭 타입처럼 동작해요. 예를 들어 심볼 int의 타입은 typedesc[int]예요. 평범한 제네릭 타입처럼 제네릭 파라미터를 생략하면 typedesc는 "모든 타입"이라는 타입 클래스를 뜻해요. 문법 편의를 위해 typedesc를 한정자(modifier)로 쓸 수도 있어요.

typedesc 파라미터를 가진 프로시저는 암시적으로 제네릭으로 간주돼요. 공급된 타입의 고유한 조합마다 인스턴스화되고, 프로시저 본문 안에서 각 파라미터의 이름은 바인딩된 구체 타입을 가리켜요:

proc new(T: typedesc): ref T =
  echo "allocating ", T.name
  new(result)

var n = Node.new
var tree = new(BinaryTree[int])

여러 타입 파라미터가 있으면 그것들은 서로 다른 타입에 자유롭게 바인딩돼요. bind-once(한 번만 바인딩) 동작을 강제하고 싶다면 명시적인 제네릭 파라미터를 쓰면 돼요:

proc acceptOnlyTypePairs[T, U](A, B: typedesc[T]; C, D: typedesc[U])

일단 바인딩되면 타입 파라미터는 프로시저 시그니처의 나머지 부분에도 등장할 수 있어요:

template declareVariableWithType(T: typedesc, value: T) =
  var x: T = value

declareVariableWithType int, 42

타입 파라미터에 매칭될 타입의 집합을 제약해서 오버로드 해석을 더 세밀하게 조절할 수도 있어요. 실제로는 템플릿으로 타입에 속성을 붙이는 방식으로 동작하는데, 제약은 구체 타입이거나 타입 클래스일 수 있어요:

template maxval(T: typedesc[int]): int = high(int)
template maxval(T: typedesc[float]): float = Inf

var i = int.maxval
var f = float.maxval
when false:
  var s = string.maxval # error, maxval is not implemented for string

template isNumber(t: typedesc[object]): string = "Don't think so."
template isNumber(t: typedesc[SomeInteger]): string = "Yes!"
template isNumber(t: typedesc[SomeFloat]): string = "Maybe, could be NaN."

echo "is int a number? ", isNumber(int)
echo "is float a number? ", isNumber(float)
echo "is RootObj a number? ", isNumber(RootObj)

typedesc를 매크로에 넘기는 것도 거의 동일하지만, 한 가지 차이가 있어요. 매크로는 제네릭으로 인스턴스화되지 않아요. 타입 표현식이 다른 것들과 마찬가지로 NimNode로 그대로 매크로에 전달돼요:

import std/macros

macro forwardType(arg: typedesc): typedesc =
  # `arg` is of type `NimNode`
  let tmp: NimNode = arg
  result = tmp

var tmp: forwardType(int)

typeof 연산자

참고: typeof(x)는 역사적인 이유로 type(x)라고 쓸 수도 있지만, type(x)는 권장되지 않아요.

표현식에서 typeof 값을 만들어 그 표현식의 타입을 얻을 수 있어요. 다른 많은 언어에서는 이걸 typeof 연산자라고 부르죠:

var x = 0
var y: typeof(x) # y has type int

typeof로 프로시저/이터레이터/컨버터 호출 c(X)(X는 비어 있을 수도 있는 인자 목록)의 결과 타입을 판별할 때는, c이터레이터인 해석이 다른 해석보다 우선돼요. 이 동작은 typeof의 두 번째 인자로 typeOfProc를 넘기면 바꿀 수 있어요:

iterator split(s: string): string = discard
proc split(s: string): seq[string] = discard

# since an iterator is the preferred interpretation, this has the type `string`:
assert typeof("a b c".split) is string

assert typeof("a b c".split, typeOfProc) is seq[string]

여기서 같은 이름 split이 이터레이터와 프로시저 둘 다로 존재해요. 기본적으로는 이터레이터 해석이 우선이라 typeof(...).splitstring이 되고, typeOfProc를 넘기면 프로시저의 결과 타입인 seq[string]을 돌려줘요.

더 알아보기 (Learn more)