네이티브 표현형을 가진 타입
네이티브 표현형을 가진 타입 (Types with native representation)
Raku는 메모리에서 고정되고 알려진 표현형(representation)을 가지는 네이티브(native) 타입들을 제공해요. 이 문서에서는 어떤 네이티브 타입들이 있는지, 그리고 그것들을 어떻게 쓸 수 있는지 살펴볼게요. 네이티브 숫자(numerics)에 대한 더 자세한 내용은 native numerics 페이지도 함께 확인해보세요.
본문
네이티브 표현형을 가진 타입 (Types with native representation)
Raku의 몇몇 간단한 타입은 네이티브 표현형을 가져요. 이는 컴파일러, 운영체제, 머신이 제공하는 C 언어 표현형을 사용하겠다는 뜻이에요. 사용 가능한 네이티브 타입은 다음 네 가지예요:
| 타입 | 의미 |
|---|---|
int |
Int와 동일 (범위가 제한됨) |
uint |
unsigned 트레잇이 붙은 Int와 동일 (범위 제한) |
num |
Num과 동일 |
str |
Str과 동일 |
다만 이 타입들은 NativeCall 인터페이스가 요구하는 크기를 반드시 가지는 건 아니에요. 예를 들어 Raku의 int는 8바이트일 수 있는데 C의 int는 4바이트뿐이죠. 네이티브 호출에서는 위에 나열한 int나 num 대신 아래에서 다룰 크기 지정 타입들을 사용해야 해요.
보통 이런 변수들은 일반 스칼라 변수와 똑같이 동작해요. 이를 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)를 구분할 수는 없어요. 다시 말해 Int와 int 후보는 가질 수 있지만, 예컨대 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]»
네이티브 array는 Iterable과 Positional로 동작하지만 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 프로그램 안에서 네이티브 포인터가 필요한 곳이라면 어디든 쓸 수 있어요.
이런 포인터가 필요한 네이티브 함수에서 써야 한다면, nativecast로 Blob을 이 포인터 종류로 변환할 수도 있어요:
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];
}
이 코드는 기대한 대로 배열의 다섯 요소를 출력해요.