컨테이너
컨테이너 (Containers)
이 문서는 Raku에서 원시 데이터(raw data), 변수, 컨테이너가 서로 어떻게 관계 맺는지 저수준에서 설명해요. Raku에서 쓰이는 서로 다른 컨테이너 타입과, 여기에 적용할 수 있는 할당(assign), 바인딩(bind), 평탄화(flatten) 같은 동작을 다루고요. 자기 참조 데이터, 타입 제약, 커스텀 컨테이너 같은 좀 더 고급 주제는 끝부분에서 다룹니다.
Raku의 순서 있는 컨테이너 종류를 더 깊이 보고 싶다면 lists, sequences, and arrays 문서를, 순서 없는 컨테이너는 sets, bags, and mixes 문서를 보세요.
변수란 무엇일까요? (What is a variable?)
어떤 사람들은 "모든 게 객체다"라고 말하길 좋아해요. 하지만 사실 변수는 Raku에서 사용자에게 노출되는 객체가 아닙니다.
컴파일러가 my $x 같은 변수 스코프 선언을 만나면, 그것을 내부 심볼 테이블에 등록해요. 이 내부 심볼 테이블은 선언되지 않은 변수를 감지하고, 그 변수에 대한 코드 생성을 올바른 스코프에 연결하는 데 쓰입니다.
런타임에는 변수가 렉시컬 패드(lexical pad), 줄여서 *렉스패드(lexpad)*의 항목으로 나타나요. 이것은 각 변수에 대한 포인터를 저장하는 스코프별 데이터 구조입니다.
my $x의 경우, 변수 $x의 lexpad 항목은 Scalar 타입의 객체, 보통 *컨테이너(the container)*라고 불리는 것을 가리키는 포인터예요.
스칼라 컨테이너 (Scalar containers)
Scalar 타입의 객체는 Raku 곳곳에 있지만, 직접 객체로 보는 일은 드물어요. 왜냐하면 대부분의 연산이 *디컨테이너화(decontainerize)*를 하거든요. 즉 Scalar 컨테이너 자체가 아니라 그 내용물에 대해 동작한다는 뜻이에요.
이런 코드에서
my $x = 42;
say $x;
할당 $x = 42는 Int 객체 42에 대한 포인터를, $x의 lexpad 항목이 가리키는 스칼라 컨테이너에 저장합니다.
할당 연산자는 왼쪽의 컨테이너에게 오른쪽 값을 저장하라고 요청해요. 정확히 무슨 의미인지는 컨테이너 타입이 정합니다. Scalar에게는 "이전에 저장된 값을 새 값으로 교체해라"라는 뜻이에요.
서브루틴 시그니처가 컨테이너를 넘겨주는 것을 허용한다는 점에 주목하세요.
sub f($a is rw) {
$a = 23;
}
my $x = 42;
f($x);
say $x; # OUTPUT: «23»
서브루틴 안에서 $a의 lexpad 항목은, 밖에서 $x가 가리키는 것과 같은 컨테이너를 가리켜요. 그래서 $a에 값을 할당하면 $x의 내용도 바뀌는 겁니다.
마찬가지로, 루틴이 is rw로 표시되어 있으면 컨테이너를 반환할 수 있어요.
my $x = 23;
sub f() is rw { $x };
f() = 42;
say $x; # OUTPUT: «42»
명시적 반환의 경우에는 return 대신 return-rw를 써야 해요.
컨테이너를 반환하는 것이 바로 is rw 속성 접근자(accessor)가 동작하는 방식이에요. 그래서
class A {
has $.attr is rw;
}
는 다음과 같습니다.
class A {
has $!attr;
method attr() is rw { $!attr }
}
스칼라 컨테이너는 타입 검사와 대부분의 읽기 전용 접근에 투명해요. .VAR를 쓰면 그 모습이 드러나죠.
my $x = 42;
say $x.^name; # OUTPUT: «Int»
say $x.VAR.^name; # OUTPUT: «Scalar»
그리고 파라미터의 is rw는 쓰기 가능한 Scalar 컨테이너가 있어야 해요.
sub f($x is rw) { say $x };
f 42;
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Parameter::RW: Parameter '$x' expected a writable container, but got Int value»
호출 가능 컨테이너 (Callable containers)
호출 가능 컨테이너는 Routine 호출의 문법과, 컨테이너에 저장된 객체의 CALL-ME 메서드의 실제 호출 사이의 다리 역할을 해요. 시길(sigil) &는 컨테이너를 선언할 때 필요하고, Callable을 실행할 때는 빼야 합니다. 기본 타입 제약은 Callable이에요.
my &callable = -> $ν { say "$ν is ", $ν ~~ Int ?? "whole" !! "not whole" }
callable(⅓); # OUTPUT: «0.333333 is not whole»
callable(3); # OUTPUT: «3 is whole»
컨테이너에 저장된 값을 가리킬 때는 시길을 제공해야 해요. 이 덕분에 Routine들을 호출의 인자로 쓸 수 있게 됩니다.
sub f() {}
my &g = sub {}
sub caller(&c1, &c2){ c1, c2 }
caller(&f, &g);
바인딩 (Binding)
할당 옆에, Raku는 := 연산자로 *바인딩(binding)*도 지원해요. 값이나 컨테이너를 변수에 바인딩하면, 변수의 lexpad 항목이 수정됩니다(그것이 가리키는 컨테이너만 바뀌는 게 아니라요). 만약 이렇게 쓰면
my $x := 42;
$x의 lexpad 항목이 Int 42를 직접 가리켜요. 그 뜻은 이제 할당을 할 수 없다는 걸 의미합니다.
my $x := 42;
$x = 23;
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot assign to an immutable value»
변수를 다른 변수에 바인딩할 수도 있어요.
my $a = 0;
my $b = 0;
$a := $b;
$b = 42;
say $a; # OUTPUT: «42»
여기서는 최초 바인딩 이후 $a와 $b의 lexpad 항목이 같은 스칼라 컨테이너를 가리켜서, 한 변수에 할당하면 다른 변수의 내용도 바뀝니다.
이 상황은 이미 봤던 거예요. is rw로 표시된 시그니처 파라미터에서 정확히 일어났던 일이죠.
시길이 없는 변수와 is raw 특질(trait)이 붙은 파라미터는 (=든 :=든) 항상 바인딩합니다.
my $a = 42;
my \b = $a;
b++;
say $a; # OUTPUT: «43»
sub f($c is raw) { $c++ }
f($a);
say $a; # OUTPUT: «44»
스칼라 컨테이너와 리스트류 (Scalar containers and listy things)
Raku에는 의미가 조금씩 다른 여러 위치형(positional) 컨테이너 타입이 있어요. 가장 기본적인 것은 콤마 연산자가 만드는 List입니다.
say (1, 2, 3).^name; # OUTPUT: «List»
리스트는 불변이에요. 즉 리스트의 원소 개수를 바꿀 수 없어요. 하지만 어떤 원소가 우연히 스칼라 컨테이너라면, 그 원소에는 여전히 할당할 수 있습니다.
my $x = 42;
($x, 1, 2)[0] = 23;
say $x; # OUTPUT: «23»
($x, 1, 2)[1] = 23; # Cannot modify an immutable value
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Int»
그래서 리스트는 자신의 원소가 값인지 컨테이너인지 신경 쓰지 않아요. 그냥 주어진 것을 저장하고 꺼내줄 뿐이죠.
리스트는 게으를 수도 있어요. 그 경우 끝부분의 원소들은 이터레이터에서 필요할 때 생성됩니다.
Array는 리스트와 거의 같지만, 모든 원소를 강제로 컨테이너로 만든다는 점이 달라요. 그래서 항상 원소에 할당할 수 있습니다.
my @a = 1, 2, 3;
@a[0] = 42;
say @a; # OUTPUT: «[42 2 3]»
@a는 실제로 스칼라 컨테이너 세 개를 저장해요. @a[0]은 그중 하나를 반환하고, 할당 연산자는 그 컨테이너에 저장된 정수 값을 새 값인 42로 바꿉니다.
배열 변수에 할당하기와 바인딩하기 (Assigning and binding to array variables)
스칼라 변수에 할당하는 것과 배열 변수에 할당하는 것은 둘 다 같은 일을 해요. 옛 값(들)을 버리고 새 값(들)을 넣는 거죠.
그래도 둘이 얼마나 다른지 쉽게 관찰할 수 있어요.
my $x = 42; say $x.^name; # OUTPUT: «Int»
my @a = 42; say @a.^name; # OUTPUT: «Array»
그 이유는 Scalar 컨테이너 타입이 자신을 잘 숨기지만, Array는 그런 노력을 하지 않기 때문이에요. 또한 배열 변수에 할당하는 것은 강제적(coercive)이라서, 배열이 아닌 값을 배열 변수에 할당할 수 있습니다.
Array가 아닌 것을 배열 변수에 넣으려면 바인딩하면 돼요.
my @a := (1, 2, 3);
say @a.^name; # OUTPUT: «List»
배열 원소에 바인딩하기 (Binding to array elements)
재미있는 사실로, Raku는 배열 원소에 바인딩하는 것을 지원해요.
my @a = (1, 2, 3);
@a[0] := my $x;
$x = 42;
say @a; # OUTPUT: «[42 2 3]»
앞선 설명을 읽고 이해했다면, 이게 어떻게 가능한지 궁금할 거예요. 어쨌든 변수에 바인딩하려면 그 변수에 대한 lexpad 항목이 필요한데, 배열에 대한 lexpad 항목은 있지만 배열의 각 원소에 대한 lexpad 항목은 없으니까요. 런타임에 lexpad를 늘릴 수 없기 때문이에요.
정답은, 배열 원소에 바인딩하는 것이 문법 수준에서 인식되어, 일반적인 바인딩 연산용 코드 대신 특수 메서드(이름은 BIND-POS)가 배열에 호출된다는 점이에요. 이 메서드가 배열 원소에 대한 바인딩을 처리합니다. 해시 원소에 바인딩하는 BIND-KEY라는 동등한 메서드도 있고요.
지원되긴 하지만, 일반적으로 컨테이너가 아닌 것을 배열 원소에 직접 바인딩하는 것은 피해야 해요. 나중에 배열을 사용할 때 직관에 반하는 결과를 낼 수 있으니까요.
my @a = (1, 2, 3);
@a[0] := 42; # This is not recommended, use assignment instead.
my $b := 42;
@a[1] := $b; # Nor is this.
@a[2] = $b; # ...but this is fine.
@a[1, 2] := 1, 2; # runtime error: X::Bind::Slice
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Bind::Slice: Cannot bind to Array slice»
List와 Array를 섞는 연산들은 일반적으로 이런 일이 우연히 일어나지 않게 보호합니다.
평탄화, 항목, 컨테이너 (Flattening, items and containers)
Raku의 %와 @ 시길은 일반적으로 반복 구조에 '여러 값'을 나타내는 반면, $ 시길은 '단일 값'을 나타내요.
my @a = 1, 2, 3;
for @a { }; # 3 iterations
my $a = (1, 2, 3);
for $a { }; # 1 iteration
@ 시길 변수는 리스트 컨텍스트에서 평탄화되지 않아요.
my @a = 1, 2, 3;
my @b = @a, 4, 5;
say @b.elems; # OUTPUT: «3»
스칼라 컨테이너 안에 있지 않은 부리스트를 평탄화하는 연산이 있어요. 슬러피 파라미터(*@a)와 flat의 명시적 호출이 그것들이에요.
my @a = 1, 2, 3;
say (flat @a, 4, 5).elems; # OUTPUT: «5»
sub f(*@x) { @x.elems };
say f @a, 4, 5; # OUTPUT: «5»
|를 써서 Slip을 만들 수도 있는데, 그렇게 하면 리스트를 다른 리스트에 끼워 넣어요.
my @l := 1, 2, (3, 4, (5, 6)), [7, 8, (9, 10)];
say (|@l, 11, 12); # OUTPUT: «(1 2 (3 4 (5 6)) [7 8 (9 10)] 11 12)»
say (flat @l, 11, 12) # OUTPUT: «(1 2 3 4 5 6 7 8 (9 10) 11 12)»
첫 번째 경우 @l의 각 원소가 결과 리스트의 대응 원소로 *슬립(slipped)*돼요. 반면 flat은 포함된 배열의 원소까지 포함해서 모든 원소를 평탄화하는데, (9 10)은 예외입니다.
앞서 언급했듯이 스칼라 컨테이너가 그 평탄화를 막아요.
sub f(*@x) { @x.elems };
my @a = 1, 2, 3;
say f $@a, 4, 5; # OUTPUT: «3»
@ 문자는 접두사로도 쓸 수 있는데, 인자를 리스트로 강제 변환해서 스칼라 컨테이너를 제거해요.
my $x = (1, 2, 3);
.say for @$x; # 3 iterations
다만 리스트가 아닌 항목을 디컨테이너화할 때는 decont 연산자 <>가 더 적절해요.
my $x = ^Inf .grep: *.is-prime;
say "$_ is prime" for @$x; # WRONG! List keeps values, thus leaking memory
say "$_ is prime" for $x<>; # RIGHT. Simply decontainerize the Seq
my $y := ^Inf .grep: *.is-prime; # Even better; no Scalars involved at all
메서드는 일반적으로 자신의 invocant가 스칼라 안에 있든 없든 신경 쓰지 않아요.
my $x = (1, 2, 3);
$x.map(*.say); # 3 iterations
위 코드는 한 개가 아니라 세 원소의 리스트에 대해 map을 수행합니다.
자기 참조 데이터 (Self-referential data)
Array와 Hash를 포함한 컨테이너 타입들은 자기 참조 구조를 만들 수 있게 해줘요.
my @a;
@a[0] = @a;
put @a.raku;
# OUTPUT: «((my @Array_75093712) = [@Array_75093712,])»
Raku가 자기 참조 데이터의 생성과 사용을 막지는 않지만, 그렇게 하면 데이터를 덤프하려다 무한 루프에 빠질 수 있어요. 최후의 수단으로 Promises를 써서 timeout을 처리할 수 있습니다.
타입 제약 (Type constraints)
모든 컨테이너는 타입 객체나 subset 형태의 타입 제약을 가질 수 있어요. 둘 다 선언자(declarator)와 변수 이름 사이, 또는 of 특질 뒤에 놓을 수 있습니다. 이 제약은 변수의 속성이지 컨테이너의 속성이 아니에요.
subset Three-letter of Str where .chars == 3;
my Three-letter $acronym = "ÞFL";
이 경우 타입 제약은 (컴파일 타입으로 정의된) subset Three-letter입니다.
Scalar 컨테이너의 기본 타입 제약은 Mu예요. 컨테이너의 타입 제약을 들여다보는 것은 .VAR.of 메서드가 제공하는데, @와 % 시길 변수의 경우 값에 대한 제약을 줍니다.
my Str $x;
say $x.VAR.of; # OUTPUT: «(Str)»
my Num @a;
say @a.VAR.of; # OUTPUT: «(Num)»
my Int %h;
say %h.VAR.of; # OUTPUT: «(Int)»
정의됨(definedness) 제약 (Definedness constraints)
컨테이너는 변수가 정의되어 있도록 강제할 수도 있어요. 선언에 스마일리를 넣으면 됩니다.
my Int:D $def = 3;
say $def; # OUTPUT: «3»
$def = Int; # Typecheck failure
선언에서 변수를 초기화하는 것도 필요해요. 어쨌든 정의되지 않은 채로 둘 수는 없으니까요.
한 스코프에서 선언된 모든 변수에 이 제약을 적용하려면 default defined variables pragma를 쓸 수도 있어요. 다른 언어(변수가 항상 정의되어 있는)에서 온 사람들이라면 한 번 볼 만합니다.
커스텀 컨테이너 (Custom containers)
커스텀 컨테이너를 제공하기 위해 Raku는 Proxy 클래스를 써요. 생성자는 두 인자를 받는데, 컨테이너에서 값을 가져오거나(fetch) 저장할(stor) 때 호출되는 메서드를 가리키는 FETCH와 STORE예요. 타입 검사는 컨테이너 자체가 하지 않으며, 읽기 전용 같은 다른 제약도 깨질 수 있어요. 따라서 반환되는 값은 바인딩된 변수의 타입과 같은 타입이어야 해요. Raku에서 타입을 다루려면 타입 캡처를 사용할 수 있습니다.
sub lucky(::T $type) {
my T $c-value; # closure variable
return-rw Proxy.new(
FETCH => method () { $c-value },
STORE => method (T $new-value) {
X::OutOfRange.new(what => 'number', got => '13', range => '-∞..12, 14..∞').throw
if $new-value == 13;
$c-value = $new-value;
}
);
}
my Int $a := lucky(Int);
say $a = 12; # OUTPUT: «12»
say $a = 'FOO'; # X::TypeCheck::Binding
say $a = 13; # X::OutOfRange
CATCH { default { say .^name, ': ', .Str } };