네이티브 표현형을 가진 타입

네이티브 표현형을 가진 타입 (Types with native representation)

Raku는 메모리에서 고정되고 알려진 표현형(representation)을 가지는 네이티브(native) 타입들을 제공해요. 이 문서에서는 어떤 네이티브 타입들이 있는지, 그리고 그것들을 어떻게 쓸 수 있는지 살펴볼게요. 네이티브 숫자(numerics)에 대한 더 자세한 내용은 native numerics 페이지도 함께 확인해보세요.

출처: Raku Documentation — Types with native representation

본문

네이티브 표현형을 가진 타입 (Types with native representation)

Raku의 몇몇 간단한 타입은 네이티브 표현형을 가져요. 이는 컴파일러, 운영체제, 머신이 제공하는 C 언어 표현형을 사용하겠다는 뜻이에요. 사용 가능한 네이티브 타입은 다음 네 가지예요:

타입 의미
int Int와 동일 (범위가 제한됨)
uint unsigned 트레잇이 붙은 Int와 동일 (범위 제한)
num Num과 동일
str Str과 동일

다만 이 타입들은 NativeCall 인터페이스가 요구하는 크기를 반드시 가지는 건 아니에요. 예를 들어 Raku의 int는 8바이트일 수 있는데 C의 int는 4바이트뿐이죠. 네이티브 호출에서는 위에 나열한 intnum 대신 아래에서 다룰 크기 지정 타입들을 사용해야 해요.

보통 이런 변수들은 일반 스칼라 변수와 똑같이 동작해요. 이를 auto-boxing이라고 불러요. 다만 차이가 몇 가지 있는데, 핵심은 여러분이 선언하는 게 실제 타입이 아니라 "어떻게 표현될지"라는 점이에요. 첫 번째 차이는, 실제 타입은 네이티브 타입이 아니라 그에 대응하는 타입이라는 거예요.

my int $intillo = 3;
say $intillo.^name; # OUTPUT: «Int␤»

따라서 이 변수들은 네이티브 타입이 아니라 그에 대응하는(auto-boxed) 타입과 스마트매치된다는 뜻이에요:

my str $strillo = "tres";
say $strillo ~~ str; # OUTPUT: «False␤»
say $strillo ~~ Str; # OUTPUT: «True␤»

그리고 네이티브가 아닌 타입과 달리, 항상 기본값(default value)을 가져요:

say (my Str $); # OUTPUT: «(Str)␤»
say (my str $); # OUTPUT: «␤»
say (my num $); # OUTPUT: «0␤»

참고: v6.c에서는 num의 기본값이 NaN이었을 거예요.

이런 차이는 네이티브 타입이 값을 감싼 채 타입에 대한 메타데이터를 전혀 가지지 않기 때문에 생겨요 — 네이티브는 그냥 값일 뿐이에요. multi-dispatch에서 네이티브 후보(candidate)를 가질 수는 있지만, 같은 네이티브 타입의 크기나 부호(signedness)를 구분할 수는 없어요. 다시 말해 Intint 후보는 가질 수 있지만, 예컨대 int, uint, atomicint, int64 후보 사이에는 모호성(ambiguity)이 생겨요.

네이티브 변수는 바인딩(bind)할 수도 없어요. my num $numillo := 3.5처럼 시도하면 Cannot bind to natively typed variable '$variable-name'; use assignment instead 예외가 발생해요.

네이티브 타입은 복합(composite)일 수도 있어요.

my int @intillos = ^10_000_000;
say [+] @intillos; # OUTPUT: «49999995000000␤»

이 경우 네이티브성은 복합 타입까지 확장되는데, 그 타입을 array라고 불러요.

my num @many-pi  = ^8 »*» π ; say @many-pi.^name;  # OUTPUT: «array[num]␤»

네이티브 arrayIterablePositional로 동작하지만 List의 하위클래스는 아니에요. 그래서 Array와 비슷하게 동작하는데, 예를 들어 shape(모양)을 줄 수 있어요:

my str @letter-pairs[10] = 'a'..'j' Z~ 'A'..'J';
say @letter-pairs.raku;
# OUTPUT: «array[str].new(:shape(10,), ["aA", "bB", "cC", "dD", "eE", "fF", "gG", "hH", "iI", "jJ"])␤»

str의 네이티브 배열은 6.d 버전부터만 사용할 수 있어요.

이 네이티브 타입들은 클래스의 속성(attribute)으로도 쓸 수 있고, 그렇게 하면 BUILD 같은 서브메서드에서 바인딩 대상으로 사용할 수 있어요:

class Foo {
    has num $.numillo;
    submethod BUILD( :$!numillo = 3.5e0 ) {}
};
my $foo = Foo.new;
say $foo.raku; # OUTPUT: «Foo.new(numillo => 3.5e0)␤»

크기를 가진 네이티브 표현형 타입 (Types with native representation and size)

네이티브 표현형 타입에 대해 앞에서 말한 내용이 여기에도 그대로 적용돼요. 이 타입들도 Raku 타입으로 auto-boxing 되고, 바인딩할 수는 없어요. 하지만 아래 표에 나열된 이 타입들은 NativeCall 함수에서 쓸 수 있다는 특징이 있어요.

타입 C에서의 의미
int8 (int8_t)
int16 (int16_t)
int32 (int32_t)
int64 (int64_t)
byte, uint8 (uint8_t)
uint16 (uint16_t)
uint32 (uint32_t)
uint64 (uint64_t)
num32 (float)
num64 (double)

이 타입들은 플랫폼과 무관하게 고정된 크기 표현을 가지므로, 네이티브 호출에 안전하게 사용할 수 있어요. 원한다면 다른 환경에서 쓰는 걸 막을 일도 없고요. 위 타입들과 마찬가지로, 이 타입의 변수에 값을 할당할 때도 그 크기를 고려해야 해요:

my byte $intillo = 257;
say $intillo; # OUTPUT: «1␤»

byte는 8비트만 담을 수 있으므로, 원래 값을 256으로 나눈 나머지(modulo)가 결과로 할당돼요. 위 출력이 바로 그 결과예요.

크기가 선언된 네이티브 타입과 그렇지 않은 타입의 핵심 차이는 선언에서 is nativesize를 쓰느냐예요. 예를 들어 int8은 다음과 같이 선언돼요:

my native int8 is repr('P6int') is Int is nativesize(8) { }

즉 정수 표현(P6int)과 함께 겨우 8비트의 네이티브 크기를 쓰겠다는 뜻이에요. 다만 이 트레잇은 Raku 사양의 일부가 아니므로 여러분 프로그램에서 쓰라고 만든 건 아니라는 점 기억해두세요.

void 타입 (The void type)

네이티브 void 타입은 C의 동명 타입에 대응해요. 유효한 타입이므로 표현식에서 쓸 수 있어요:

use NativeCall;
my void $nothing;
say $nothing.raku; # OUTPUT: «NativeCall::Types::void␤»

실제로는 Uninstantiable 타입이라 혼자 쓰이는 일은 드물고, 반환 타입으로 쓰는 것은 명시적으로 금지돼 있어요. 대신 C의 void * 포인터에 해당하는 타입 포인터에서 흔히 발견돼요.

sub malloc( int32 $size --> Pointer[void] ) is native { * };
my Pointer[void] $for-malloc = malloc( 32 );
say $for-malloc.raku;

예시에서 보듯, 이 타입은 원래 포인터가 가리키는 대상(여기서는 void)을 매개변수로 받는 Pointer 롤로 표현돼요. 이 롤은 네이티브 포인터를 나타내며, Raku 프로그램 안에서 네이티브 포인터가 필요한 곳이라면 어디든 쓸 수 있어요.

이런 포인터가 필요한 네이티브 함수에서 써야 한다면, nativecastBlob을 이 포인터 종류로 변환할 수도 있어요:

use NativeCall;
my Pointer[void] $native = nativecast(Pointer[void], Blob.new(0x22, 0x33));

다만 그 외에는 제공하는 기능이 꽤 제한적이에요. void 포인터는 역참조(dereference)할 수 없거든요:

use NativeCall;
my Pointer[void] $native = nativecast(Pointer[void], Buf.new(0x22, 0x33));
say $native.deref; # ERROR OUTPUT: «Internal error: unhandled target type␤»

원자 타입 (Atomic types)

여기서 atomic(원자)이라는 말은 스레딩 환경에서 안전하게 동작한다는 뜻이에요. Raku는 atomicint 타입과 몇몇 연산을 제공하는데, 이 둘이 합쳐져 원자성을 보장해요. 자세한 내용은 Numerics 페이지의 원자 연산 섹션을 참고하세요.

Rakudo 전용 네이티브 타입 (Rakudo specific native types)

이 섹션에서 설명하는 타입들은 Rakudo 고유의 것이에요. 따라서 다른 구현체에 있으리라는 보장도, 미래 버전에서 그대로 남아 있으리라는 보장도 없어요.

타입 C에서의 의미
long (long)
longlong (longlong)
ulong (unsigned long)
ulonglong (unsigned longlong)
size_t (unsigned size_t)
ssize_t (size_t)
bool (bool)

네이티브 C에서 쓰는 것과 같은 방식으로 사용할 수 있어요:

use NativeCall;

my $just-an-array = CArray[int32].new( 1, 2, 3, 4, 5 );

loop ( my size_t $i = 0; $i < $just-an-array.elems; $i++ ) {
    say $just-an-array[$i];
}

이 코드는 기대한 대로 배열의 다섯 요소를 출력해요.