subscripts — 첨자(subscript)로 요소 접근하기
subscripts — 첨자(subscript)로 요소 접근하기
컬렉션이나 데이터 구조에서 특정 요소(또는 요소 묶음)를 가리켜야 할 때가 많아요. 수학 표기법에서 벡터 v의 성분을 v₁, v₂, v₃로 부르는 것에서 빌려와서, Raku에서는 이 개념을 subscripting(첨자화) 또는 indexing(인덱싱)이라고 불러요. 이 페이지에서는 Raku의 두 가지 첨자 인터페이스인 Positional([ ])과 Associative({ })를 자세히 다룰게요.
기초
Raku는 두 가지 보편적인 첨자 인터페이스를 제공해요.
| 표기 | 요소를 식별하는 기준 | 인터페이스 이름 | 지원 타입 |
|---|---|---|---|
| [ ] | 0부터 시작하는 인덱스 | Positional | Array, List, Buf, Match, ... |
| { } | 문자열 또는 객체 키 | Associative | Hash, Bag, Mix, Match, ... |
Positional 첨자화
Positional 첨자화(postcircumfix [ ]를 통해)는 순서 있는 컬렉션의 요소를 위치로 가리켜요. 인덱스 0이 첫 요소, 1이 둘째 요소, 그리고 계속 이어져요:
my @chores = "buy groceries", "feed dog", "wash car";
say @chores[0]; # OUTPUT: «buy groceries»
say @chores[1]; # OUTPUT: «feed dog»
say @chores[2]; # OUTPUT: «wash car»
참고로 Rakudo의 첨자화 구현은 64비트 빌드에서 63비트만 허용해요. 더 큰 첨자가 필요하면 Array::Sparse를 쓰거나 아래에서 볼 Associative 첨자화를 쓰는 Hash를 사용하면 돼요.
Associative 첨자화
Associative 첨자화(postcircumfix { }를 통해)는 컬렉션이 요소를 특정 순서로 유지할 필요가 없어요. 대신 각 값을 가리키는 고유한 키를 사용해요. 키의 성격은 컬렉션에 따라 달라져요. 예를 들어 표준 Hash는 문자열 키를 쓰고, Mix는 임의의 객체를 키로 허용해요:
my %grade = Zoe => "C", Ben => "B+";
say %grade{"Zoe"}; # OUTPUT: «C»
say %grade{"Ben"}; # OUTPUT: «B+»
my $stats = ( Date.today => 4.18, Date.new(2015, 4, 5) => 17.253 ).Mix;
say $stats{ Date.new(2015, 4, 4) + 1 }; # OUTPUT: «17.253»
{ }에 단일 단어 문자열 키를 전달할 때는, 각괄호 단어 quoting 구문을 postcircumfix 연산자인 것처럼 쓰면 돼요:
my %grade = Zoe => "C", Ben => "B+";
say %grade<Zoe>; # OUTPUT: «C»
say %grade<Ben>; # OUTPUT: «B+»
이건 컴파일 타임에 해당 { } 형태로 바뀌는 문법적 설탕일 뿐이에요:
%hash<foo bar>; # same as %hash{ <foo bar> }
%hash«foo "$var"»; # same as %hash{ «foo "$var"» }
%hash<<foo "$var">>; # same as %hash{ <<foo "$var">> }
위에서 => 연산자로 Zoe에 따옴표를 붙이지 않아도 됐던 걸 눈치챘을 거예요. 하지만 같은 연산자가 Date.new(2015, 4, 5)에는 보이지 않는 따옴표를 붙이지 않았고, $stats{ Date.new(2015, 4, 4) + 1 }로 같은 요소를 찾을 수 있었어요. =>는 단일 단어에만 보이지 않는 따옴표를 붙이기 때문인데, 여기서 '단어'란 식별자/이름을 뜻해요. => 연산자는 그 이름을 가진 함수를 실수로 호출하거나 상수를 쓰는 걸 막아 주는 역할을 해요.
해시 첨자는 =>와 같은 일을 하지 않아요. 기본 Hash는 다른 언어에서 새 사용자가 기대하는 방식대로, 그리고 사용 편의를 위해 그렇게 동작하도록 만들어져 있어요. 기본 Hash에서 첨자는 Cool을 만들어내는 키면 그 키를 문자열로 강제 변환해요. 컬렉션에 .raku를 쓰면 키가 문자열인지 객체인지 확인할 수 있어요:
( 1 => 1 ).raku.say; # OUTPUT: «1 => 1»
my %h; %h{1} = 1; say %h.raku; # OUTPUT: «{ "1" => 1 }»
( 1/2 => 1 ).raku.say; # OUTPUT: «0.5 => 1»
my %h; %h{1/2} = 1; say %h.raku; # OUTPUT: «{ "0.5" => 1 }»
( pi => 1 ).raku.say; # OUTPUT: «:pi(1)»
my %h; %h{pi} = 1; say %h.raku; # OUTPUT: «{ "3.14159265358979" => 1 }»
단일 이름 주변의 보이지 않는 따옴표가 =>에 내장된 반면, 문자열 변환은 중괄호에 내장된 게 아니에요. 그것은 기본 Hash의 동작이에요. 모든 해시·컬렉션 타입이 그렇게 하는 건 아니에요:
my %h := MixHash.new;
%h{pi} = 1; %h.raku.say; # OUTPUT: «(3.14159265358979e0=>1).MixHash»
(=>가 문자열로 변환할 수 있는 어떤 이름이든 "부사 표기"(adverbial notation)로 페어를 만드는 데 쓸 수 있고, .raku로 보면 그렇게 나타나요. 그래서 위에서 :pi(1)이 보이는 거예요.)
첨자 적용
첨자는 변수뿐 아니라 첨자 가능한 객체를 돌려주는 어떤 표현식에도 적용할 수 있어요:
say "__Hello__".match(/__(.*)__/)[0]; # OUTPUT: «「Hello」»
say "__Hello__".match(/__(.*)__/).[0]; # same, in method notation
Positional과 associative 첨자화는 서로 배타적이지 않아요. 예를 들어 Match 객체는 둘 다 지원해요 (각각 다른 데이터 집합에 접근). 또 리스트 처리를 편하게 하기 위해 Any 클래스가 positional 첨자의 폴백 구현을 제공하는데, 호출자를 한 요소짜리 리스트처럼 취급해요. (associative 첨자에는 그런 폴백이 없어서, 그걸 지원하지 않는 객체에 적용하면 런타임 오류가 나요.)
say 42[0]; # OUTPUT: «42»
say 42<foo>; # ERROR: Type Int does not support associative indexing.
존재하지 않는 요소
첨자가 존재하지 않는 요소를 가리키면 무슨 일이 일어나는지는 컬렉션 타입에 달려 있어요. 표준 Array와 Hash 컬렉션은 값 타입 제약의 타입 객체(기본은 Any)를 돌려주지만, is default 트레잇으로 선언된 컬렉션이면 프로그래머가 선언한 값을 돌려줘요:
# no default values specified
my @array1; say @array1[10]; # OUTPUT: «(Any)»
my Int @array2; say @array2[10]; # OUTPUT: «(Int)»
my %hash1; say %hash1<foo>; # OUTPUT: «(Any)»
my Int %hash2; say %hash2<foo>; # OUTPUT: «(Int)»
# default values specified
my @array3 is default('--'); say @array3[10]; # OUTPUT: «--»
my Int @array4 is default(17); say @array4[10]; # OUTPUT: «17»
my %hash3 is default('Empty'); say %hash3<foo>; # OUTPUT: «Empty»
my Int %hash4 is default(4711); say %hash4<foo>; # OUTPUT: «4711»
다만 다른 타입의 컬렉션은 존재하지 않는 요소에 다르게 반응할 수 있어요:
say (0, 10, 20)[3]; # OUTPUT: «Nil»
say bag(<a a b b b>)<c>; # OUTPUT: «0»
say array[uint8].new(1, 2)[2] # OUTPUT: «0»
첨자 연산에서 존재하지 않는 요소를 조용히 건너뛰려면 Truncating slices와 :v 어드버브를 보면 돼요.
뒤에서부터
Positional 인덱스는 컬렉션의 처음부터 세지만, 끝을 기준으로 요소를 가리키는 표기법도 있어요. *-1은 마지막 요소, *-2는 끝에서 둘째, 그리고 계속이에요. 6.d 버전부터 *-0은 마지막 요소 바로 다음 요소를 가리켜요:
my @alphabet = 'A' .. 'Z';
say @alphabet[*-1]; # OUTPUT: «Z»
say @alphabet[*-2]; # OUTPUT: «Y»
say @alphabet[*-3]; # OUTPUT: «X»
@alphabet[*-0] = 'þ';
say @alphabet;
# OUTPUT: «[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z þ]»
주의: 별표는 사실 Whatever인데, 그게 중요해요. 다른 언어들처럼 맨 음수 정수(예: @alphabet[-1])를 넘기면 Raku에서 오류가 나요.
실제로 벌어지는 일은 이래요. *-1 같은 표현식이 Whatever-priming을 통해 코드 객체를 선언하고, [ ] 첨자가 인덱스로 코드 객체를 받으면 컬렉션 길이를 인자로 호출해서 그 결괏값을 실제 인덱스로 써요. 즉 @alphabet[*-1]은 @alphabet[@alphabet.elems - 1]이 되는 거예요.
이 말은 컬렉션 크기에 의존하는 임의의 표현식을 쓸 수 있다는 뜻이에요:
say @array[* div 2]; # select the middlemost element
say @array[$i % *]; # wrap around a given index ("modular arithmetic")
say @array[ -> $size { $i % $size } ]; # same as previous
슬라이스
컬렉션의 여러 요소에 접근해야 할 때, 별도의 첨자 연산을 여러 번 하는 대신 단축키가 있어요. 첨자에 인덱스/키의 리스트를 지정하면 같은 순서로 요소의 리스트를 돌려받는데, 이를 "슬라이스"라고 불러요.
Positional 슬라이스에서는 일반 인덱스와 끝에서부터 세는 것을 섞을 수 있어요:
my @alphabet = 'a' .. 'z';
say @alphabet[15, 4, *-9, 11].raku; # OUTPUT: «("p", "e", "r", "l")»
위의 *-number 구문에서 *는 "뒤에서부터" 섹션에서 설명한 대로 배열의 끝을 나타내요. 그래서 배열의 마지막 N개 요소를 가져오려면 포함하는 Range를 만들어야 해요.
(5802..5830).map( {.chr} )[*-10..*-5] # OUTPUT: «(ᚽ ᚾ ᚿ ᛀ ᛁ ᛂ)»
범위의 마지막 요소로 *를 쓰면 사실상 컬렉션의 마지막 요소들을 돌려줘요.
Associative 슬라이스에서는 각괄호 형태가 종종 편리해요:
my %color = kiwi => "green", banana => "yellow", cherry => "red";
say %color{"cherry", "kiwi"}; # OUTPUT: «(red green)»
say %color<cherry kiwi>; # OUTPUT: «(red green)»
say %color{*}; # OUTPUT: «(red yellow green)»
슬라이스는 첨자의 (한 차원에) 전달되는 것의 길이가 아니라 타입에 의해 제어된다는 점에 유의해요. 특히 그 타입은 다음 중 하나일 수 있어요:
[ ]에서 자르는 lazyIterable- 그에 따라 무한
Range는 자르지만, 유한Range는 보통 슬라이스를 만든다 - 전체 슬라이스(모든 키/인덱스를 지정한 것처럼)를 돌려주는
'*'(whatever-star) - 전체 슬라이스를 돌려주되 모든 리스트를 재귀적으로 평탄화하는
'**'(hyperwhatever-star, Rakudo 2024.08 릴리스부터) - 단일 요소 접근을 제공하는(슬라이스가 아닌) 다른 어떤 객체
Callable, callable이 돌려주는 무엇이든 (재귀로 이어질 수 있음)- 비어 있음(empty), Zen slice라고 알려진 전체 슬라이스
- 위와 다른 어떤 iterable, 보통 슬라이스
*와 Zen slice(빈 것) 사이의 주목할 차이는, Whatever 별표는 완전한 reification·itemization을 일으키지만 Zen slice는 그렇지 않다는 거예요. 둘 다 de-cont 합니다.
그래서 한 요소짜리 리스트도 슬라이스를 돌려주지만, 맨 스칼라 값은 그렇지 않아요:
say @alphabet[2,]; # OUTPUT: «(c)»
say @alphabet[2,].^name; # OUTPUT: «List»
say @alphabet[2]; # OUTPUT: «c»
say @alphabet[2].^name; # OUTPUT: «Str»
(Associative 첨자의 각괄호 형태가 잘 동작하는 건, 단어 quoting이 단일 단어면 Str을, 여러 단어면 List를 돌려주기 때문이에요.)
사실 첨자의 (현재 차원의) 리스트 구조는 슬라이스 연산을 통해서도 보존돼요 (단 Iterable의 종류는 아니에요 — 결과는 항상 그냥 리스트예요).
say @alphabet[0, (1..2, (3,))]; # OUTPUT: «(a ((b c) (d)))»
say @alphabet[0, (1..2, [3,])]; # OUTPUT: «(a ((b c) (d)))»
say @alphabet[flat 0, (1..2, (3,))]; # OUTPUT: «(a b c d)»
say flat @alphabet[0, (1..2, (3,))]; # OUTPUT: «(a b c d)»
Truncating slices (자르는 슬라이스)
슬라이스 첨자에서 존재하지 않는 요소를 가리키면 출력 List에 정의되지 않은 값(또는 해당 컬렉션이 존재하지 않는 요소에 돌려주기로 선택한 값)이 포함돼요:
my @letters = <a b c d e f>;
say @letters[3..7]; # OUTPUT: «(d e f (Any) (Any))»
이 동작은 언뜻 직관적이지 않아 보이지만, 현재 값이 없는 인덱스에 값을 할당하고 싶을 때 바람직해요.
my @letters;
say @letters; # OUTPUT: «[]»
@letters[^10] = 'a'..'z';
say @letters; # OUTPUT: «[a b c d e f g h i j]»
결과 슬라이스에 존재하는 요소만 포함하고 싶다면 :v 어드버브로 존재하지 않는 요소를 조용히 건너뛸 수 있어요.
my @letters = <a b c d e f>;
say @letters[3..7]:v; # OUTPUT: «(d e f)»
lazy 첨자로 컬렉션을 인덱싱할 때의 동작은 eager 첨자와 다르다는 점을 기억해야 해요. lazy 첨자로 접근하면 결과 슬라이스가 잘려요.
say @letters[lazy 3..7]; # OUTPUT: «(d e f)»
say @letters[ 3..*]; # OUTPUT: «(d e f)»
이 동작은 방대하고 잠재적으로 무한한 List의 폭주 생성과 그로 인한 메모리 부족 문제를 막기 위한 예방책이에요.
Zen slices
인덱스/키를 전혀 지정하지 않고 첨자 연산자를 객체 뒤에 놓으면, 연산자는 첨자된 객체 자체를 돌려줘요. 비어 있지만 모든 것을 돌려주므로 Zen slice라고 불러요.
Zen slicing은 Whatever-star를 전달하는 것(보통 슬라이스처럼 원래 객체의 타입과 무관하게 항상 요소의 List를 돌려주는)이나 빈 리스트(빈 슬라이스)와는 달라요:
my %bag := (orange => 1, apple => 3).Bag;
say %bag<>; # OUTPUT: «Bag(apple(3) orange)»
say %bag{}; # OUTPUT: «Bag(apple(3) orange)»
say %bag{*}; # OUTPUT: «(1 3)»
say %bag{()}; # OUTPUT: «()»
Zen slicing은 (Seq조차) reify하거나 캐시하지 않고 그냥 호출자를 돌려줘요. 보통 전체 배열/해시를 문자열에 삽입하거나 de-cont하는 데 쓰여요.
my @words = "cruel", "world";
say "Hello, @words[]!"; # OUTPUT: «Hello, cruel world!»
my $list = <a b c>;
.say for $list; # OUTPUT: «(a b c)»
.say for $list<>; # OUTPUT: «abc»
.say for $list[]; # OUTPUT: «abc»
my @fib = 1,1, * + * … *;
say @fib[]; # OUTPUT: «[...]»
여러 차원
첨자의 차원은 세미콜론으로 구분해서, 요소 리스트와 차원을 섞을 수 있어요.
my @twodim = (<a b c>, (1, 2, 3));
say @twodim;
# OUTPUT: «[(a b c) (1 2 3)]»
say @twodim[0,1;1]; # 2nd element of both lists
# OUTPUT: «(b 2)»
my %pantheon = %('Baldr' => 'consort' => 'Nanna' ,
'Bragi' => 'consort' => 'Iðunn' ,
'Nótt' => 'consort' => 'Dellingr' );
say %pantheon{'Bragi','Nótt';'consort'}; # 'consort' value for both keys
# OUTPUT: «(Iðunn Dellingr)»
다차원 첨자는 Whatever와 결합하면 중첩 리스트를 평탄화하는 데 쓸 수 있어요.
my @toomany = [[<a b>], [1, 2]];
say @toomany;
# OUTPUT: «[[a b] [1 2]]»
say @toomany[*;*];
# OUTPUT: «(a b 1 2)»
평탄화 세미콜론은 원하는 만큼 쓸 수 있어요. 세미콜론 개수만큼 중첩 수준이(최대한) 평탄화돼요:
say [[1,2,[3,4]],[4,5]][*;*]; # OUTPUT: «(1 2 [3 4] 4 5)»
say [[1,2,[3,4]],[4,5]][*;*;*;*]; # OUTPUT: «(1 2 3 4 4 5)»
첫 예제에서는 중첩 수준보다 Whatever를 하나 적게 써서, 가장 깊은 건 평탄화되지 않았어요. 두 번째는 수준 수보다 많으므로 평탄화됐죠.
다차원 첨자에서 Whatever로 범위나 "행"을 고를 수도 있어요.
my @a = [[1,2], [3,4]];
say @a[*;1]; # 2nd element of each sub list
# OUTPUT: «(2 4)»
my @a = (<1 c 6>, <2 a 4>, <5 b 3>);
say @a.sort( { $_[1] } ); # sort by 2nd column
# OUTPUT: «((2 a 4) (5 b 3) (1 c 6))»
요소 수정하기
Autovivification (자동 생성)
첨자는 "autovivification"에 참여해요. 즉 배열과 해시가 필요할 때 자동으로 생겨나서, 매 단계 컬렉션 타입을 미리 선언하지 않고도 중첩 데이터 구조를 만들 수 있게 해 줘요.
my $beatles;
$beatles{"White Album"}[0] = "Back in the U.S.S.R."; # autovivification!
say $beatles.raku; # OUTPUT: «${"White Album" => $["Back in the U.S.S.R."]}»
$beatles는 정의되지 않은 채 시작했지만, 할당에서 { }로 첨자화됐으므로 Hash 객체가 됐어요. 마찬가지로 $beatles{"White Album"}은 할당에서 [ ]로 첨자화됐으므로 Array 객체가 됐어요.
첨자화 자체는 autovivification을 일으키지 않는다는 점을 주의하세요. 첨자 체인의 결과가 할당(또는 달리 변경)될 때만 일어나요.
Binding (바인딩)
첨자 표현식은 바인딩 문장의 왼쪽에도 쓸 수 있어요. 첨자된 컬렉션의 타입이 지원한다면, 그 컬렉션의 해당 "슬롯"에 자연히 있을 값 컨테이너를 지정된 컨테이너로 바꿔치기해요.
내장 Array와 Hash 타입은 복잡한 연결(linked) 데이터 구조를 만들 수 있게 이걸 지원해요:
my @a = 10, 11, 12, 13;
my $x = 1;
@a[2] := $x; # Bound! (@a[2] and $x refer to the same container now.)
$x++; @a[2]++;
say @a; # OUTPUT: «[10 11 3 13]»
say $x; # OUTPUT: «3»
이건 lazy 데이터 구조가 더 큰 구조의 일부일 때 특히 유용해요.
my @fib = 1,1, * + * … ∞;
my @lucas = 1,3, * + * … ∞;
my %sequences;
%sequences<f> := @fib;
%sequences<l> := @lucas;
for %sequences.keys -> $s {
for ^10 -> $n {
say %sequences{$s}[100+$n*10]/%sequences{$s}[101+$n*10];
}
}
# OUTPUT: 0.6180339887498949 times 20.
이 경우 해시 키가 lazy하게 생성되는 시퀀스에 바인딩돼요. 바인딩됐다는 건 계산된 어떤 상태든 해시 값과 그에 바인딩된 시퀀스가 공유한다는 뜻이라, 이후 요소 계산이 빨라져요.
기저 메커니즘은 BIND-POS 메서드와 BIND-KEY 메서드를 보면 돼요.
Adverbs (부사)
첨자 연산의 반환값과 부수 효과는 부사로 제어할 수 있고, 관련 첨자 연산자에 정의돼 있어요.
연산자 부사의 비교적 느슨한 우선순위를 조심해야 해요. 복합 표현식에서는 괄호를 추가해야 할 수 있어요:
if $foo || %hash<key>:exists { ... } # WRONG, tries to adverb the || op
if $foo || (%hash<key>:exists) { ... } # correct
if $foo or %hash<key>:exists { ... } # also correct
지원되는 부사는 다음과 같아요.
:exists
요청한 요소의 실제 값 대신, 그 요소가 존재하는지 여부를 돌려줘요. 정의되지 않은 값을 가진 요소와 컬렉션에 전혀 없는 요소를 구분하는 데 쓸 수 있어요:
my @foo = Any, 10;
say @foo[0].defined; # OUTPUT: «False»
say @foo[0]:exists; # OUTPUT: «True»
say @foo[2]:exists; # OUTPUT: «False»
say @foo[0, 2]:exists; # OUTPUT: «(True False)»
my %fruit = apple => Any, orange => 10;
say %fruit<apple>.defined; # OUTPUT: «False»
say %fruit<apple>:exists; # OUTPUT: «True»
say %fruit<banana>:exists; # OUTPUT: «False»
say %fruit<apple banana>:exists; # OUTPUT: «(True False)»
부정해서 비존재를 시험할 수도 있어요:
say %fruit<apple banana>:!exists; # OUTPUT: «(False True)»
슬라이스의 모든 요소가 존재하는지 확인하려면 all junction을 써요:
if all %fruit<apple orange banana>:exists { ... }
다차원 배열·해시에도 쓸 수 있어요:
my @multi-dim = 1, [2, 3, [4, 5]];
say @multi-dim[1;2;0]:exists; # OUTPUT: «True»
say @multi-dim[1;2;5]:exists; # OUTPUT: «False»
my %multi-dim = 1 => { foo => { 3 => 42 } };
say %multi-dim{1;'foo';3}:exists; # OUTPUT: «True»
say %multi-dim{1;'bar';3}:exists; # OUTPUT: «False»
:exists는 :delete와 :p/:kv 부사와 결합할 수 있어요. 이 경우 동작은 그 부사들이 정하되, 돌려주는 요소 값만 그 요소의 존재를 나타내는 Bool로 바뀌어요.
기저 메커니즘은 EXISTS-POS 메서드와 EXISTS-KEY 메서드를 보면 돼요.
:delete
컬렉션에서 요소를 삭제하고, 그 값도 돌려줘요. 컬렉션이 지원하면 주어진 인덱스에 구멍(hole)을 만듭니다:
my @tens = 0, 10, 20, 30;
say @tens[3]:delete; # OUTPUT: «30»
say @tens; # OUTPUT: «[0 10 20]»
my %fruit = apple => 5, orange => 10, banana => 4, peach => 17;
say %fruit<apple>:delete; # OUTPUT: «5»
say %fruit<peach orange>:delete; # OUTPUT: «(17 10)»
say %fruit; # OUTPUT: «{banana => 4}»
Nil을 할당하면 주어진 인덱스의 컨테이너가 기본값으로 되돌아간다는 점을 기억하세요. 구멍은 만들어지지 않아요. 만들어진 구멍은 :exists로 시험할 수 있지만, 반복이 그것을 건너뛰지는 않고 대신 정의되지 않은 값을 만들어내요.
my @a = 1, 2, 3;
@a[1]:delete;
say @a[1]:exists;
# OUTPUT: «False»
.say for @a;
# OUTPUT: «1(Any)3»
부사 형태를 부정하면 요소가 실제로 삭제되지는 않아요. 이걸로 조건부 삭제 플래그를 만들 수 있어요:
say %fruit<apple> :delete($flag); # deletes the element only if $flag is
# true, but always returns the value.
:exists와 :p/:kv/:k/:v 부사와 결합할 수 있어요. 이 경우 반환값은 그 부사들이 정하되, 동시에 요소가 삭제돼요.
기저 메커니즘은 DELETE-POS 메서드와 DELETE-KEY 메서드를 보면 돼요.
associative 타입 객체에도 이 부사를 쓸 수 있지만, 실제로는 아무것도 하지 않고 Nil을 돌려줘요:
say Hash<foo>:delete; # OUTPUT: «Nil»
lazy 배열에도 쓸 수 있어요:
my @lazy-array = lazy 1, 11, 121 ... 10**100;
@lazy-array[2**24]:delete;
:p
요소의 인덱스/키와 값을 Pair 형태로 돌려주고, 존재하지 않는 요소는 조용히 건너뛰어요:
my @tens = 0, 10, 20, 30;
say @tens[1]:p; # OUTPUT: «1 => 10»
say @tens[0, 4, 2]:p; # OUTPUT: «(0 => 0 2 => 20)»
my %month = Jan => 1, Feb => 2, Mar => 3;
say %month<Feb>:p; # OUTPUT: «Feb => 2»
say %month<Jan Foo Mar>:p; # OUTPUT: «(Jan => 1 Mar => 3)»
존재하지 않는 요소를 건너뛰고 싶지 않으면 부정 형태를 써요:
say %month<Jan Foo Mar>:!p; # OUTPUT: «(Jan => 1 Foo => (Any) Mar => 3)»
:exists와 :delete 부사와 결합할 수 있어요. pairs 루틴도 참고하세요.
:kv
요소의 인덱스/키와 값을 List 형태로 돌려주고, 존재하지 않는 요소는 조용히 건너뛰어요. 슬라이스에 쓸 때 반환값은 키와 값이 번갈아 나오는 단일 평평한 리스트예요:
my @tens = 0, 10, 20, 30;
say @tens[1]:kv; # OUTPUT: «(1 10)»
say @tens[0, 4, 2]:kv; # OUTPUT: «(0 0 2 20)»
my %month = Jan => 1, Feb => 2, Mar => 3;
say %month<Feb>:kv; # OUTPUT: «(Feb 2)»
say %month<Jan Foo Mar>:kv; # OUTPUT: «(Jan 1 Mar 3)»
건너뛰고 싶지 않으면 부정 형태:
say %month<Jan Foo Mar>:!kv; # OUTPUT: «(Jan 1 Foo (Any) Mar 3)»
이 부사는 슬라이스를 반복할 때 흔히 쓰여요:
for %month<Feb Mar>:kv -> $month, $i {
say "$month had {Date.new(2015, $i, 1).days-in-month} days in 2015"
}
:exists와 :delete 부사와 결합할 수 있어요. kv 루틴도 참고하세요.
:k
요소의 값 대신 인덱스/키만 돌려주고, 존재하지 않는 요소는 조용히 건너뛰어요:
my @tens = 0, 10, 20, 30;
say @tens[1]:k; # OUTPUT: «1»
say @tens[0, 4, 2]:k; # OUTPUT: «(0 2)»
my %month = Jan => 1, Feb => 2, Mar => 3;
say %month<Feb>:k; # OUTPUT: «Feb»
say %month<Jan Foo Mar>:k; # OUTPUT: «(Jan Mar)»
건너뛰고 싶지 않으면 부정 형태:
say %month<Jan Foo Mar>:!k; # OUTPUT: «(Jan Foo Mar)»
keys 루틴도 참고하세요.
:v
요소의 (가변 값 컨테이너를 돌려줄 수도 있는 대신) 맨 값을 돌려주고, 존재하지 않는 요소는 조용히 건너뛰어요:
my @tens = 0, 10, 20, 30;
say @tens[1]:v; # OUTPUT: «10»
say @tens[0, 4, 2]:v; # OUTPUT: «(0, 20)»
@tens[3] = 31; # OK
@tens[3]:v = 31; # ERROR, Cannot modify an immutable Int (31)
my %month = Jan => 1, Feb => 2, Mar => 3;
say %month<Feb>:v; # OUTPUT: «2»
say %month<Jan Foo Mar>:v; # OUTPUT: «(1 3)»
건너뛰고 싶지 않으면 부정 형태:
say %month<Jan Foo Mar>:!v; # OUTPUT: «(1 (Any) 3)»
values 루틴도 참고하세요.
커스텀 타입
여기서 설명한 첨자 인터페이스는 Raku의 내장 컬렉션 타입 전용이 아니에요. 인덱스나 키로 데이터 접근을 제공하려는 어떤 커스텀 타입에도 (그래야 하고) 재사용할 수 있어요.
postcircumfix [ ]와 postcircumfix { } 연산자를 수동으로 오버로드하고 그 모든 마법을 다시 구현할 필요는 없어요. 대신 그 표준 구현이 내부에서 잘 정의된 저수준 메서드 집합으로 디스패치한다는 사실에 기댈 수 있어요. 예를 들어:
| 여러분이 쓰는 것 | 내부에서 호출되는 것 |
|---|---|
| %foo |
%foo.AT-KEY("aa") |
| %foo |
%foo.DELETE-KEY("aa") |
| @foo[3, 4, 5] | @foo.AT-POS(3), @foo.AT-POS(4), @foo.AT-POS(5) |
| @foo[*-1] | @foo.AT-POS(@foo.elems - 1) |
그래서 첨자화가 동작하게 하려면 커스텀 타입에 이 저수준 메서드들(아래에서 자세히 설명)만 구현하거나 위임하면 돼요.
그렇게 한다면 타입이 각각 Positional 또는 Associative 역할을 합성하게 해야 해요. 이러면 자체로 기능을 추가하지는 않지만, 해당 타입이 그에 대응하는 첨자 인터페이스를 구현한다는 걸 알리고 (검사에도 쓸 수 있고요).
커스텀 타입 예제
HTTP::Header 타입을 상상해 봐요. 특별한 동작을 가진 커스텀 클래스지만, 해시처럼 인덱싱할 수 있어요:
my $request = HTTP::Request.new(GET => "raku.org");
say $request.header.^name; # OUTPUT: «HTTP::Header»
$request.header<Accept> = "text/plain";
$request.header{'Accept-' X~ <Charset Encoding Language>} = <utf-8 gzip en>;
$request.header.push('Accept-Language' => "fr"); # like .push on a Hash
say $request.header<Accept-Language>.raku; # OUTPUT: «["en", "fr"]»
my $rawheader = $request.header.Str; # stringify according to HTTP spec
이 클래스를 구현하는 간단한 방법은 Hash 타입 속성을 하나 주고, 모든 첨자·반복 관련 기능을 그 속성에 위임하는 거예요 (커스텀 타입 제약으로 사용자가 잘못된 것을 넣지 못하게 하고요):
class HTTP::Header does Associative {
subset StrOrArrayOfStr where Str | ( Array & {.all ~~ Str} );
has %!fields of StrOrArrayOfStr
handles <AT-KEY EXISTS-KEY DELETE-KEY push
iterator list kv keys values>;
method Str { #`[not shown, for brevity] }
}
하지만 HTTP 헤더 필드 이름은 대소문자를 구분하지 않아야 해요 (그리고 camel-case가 선호되고요). *-KEY와 push 메서드를 handles 목록에서 빼내서 별도로 이렇게 구현하면 돼요:
method AT-KEY ($key) is rw { %!fields{normalize-key $key} }
method EXISTS-KEY ($key) { %!fields{normalize-key $key}:exists }
method DELETE-KEY ($key) { %!fields{normalize-key $key}:delete }
method push(*@_) { #`[not shown, for brevity] }
sub normalize-key ($key) { $key.subst(/\w+/, *.tc, :g) }
%!fields에 첨자를 쓰면 적절한 rw 컨테이너가 돌아오고, AT-KEY는 그걸 그대로 넘기면 돼요.
하지만 사용자 입력에 덜 엄격하고, 필드 값을 직접 살균(sanitize)하고 싶을 수도 있어요. 그 경우 %!fields의 StrOrArrayOfStr 타입 제약을 제거하고, 할당 시 값을 살균하는 커스텀 Proxy 컨테이너를 돌려주는 AT-KEY 구현으로 바꾸면 돼요:
multi method AT-KEY (::?CLASS:D: $key) is rw {
my $element := %!fields{normalize-key $key};
Proxy.new(
FETCH => method () { $element },
STORE => method ($value) {
$element = do given $value».split(/',' \s+/).flat {
when 1 { .[0] } # a single value is stored as a string
default { .Array } # multiple values are stored as an array
}
}
);
}
메서드를 multi로 선언하고 :D(정의된 호출자)로 제한하면, 정의되지 않은 경우가 Any가 제공하는 기본 구현(autovivification에 관여)으로 전달된다는 것을 보장해요.
Positional 첨자화를 위해 구현할 메서드
postcircumfix [ ]로 인덱스 기반 첨자화가 동작하게 하려면 최소한 elems, AT-POS, EXISTS-POS를 구현해야 해요 (그리고 아래에서 자세히 나오는 것들을 선택적으로).
method elems
multi method elems(::?CLASS:D:)
객체에 첨자 가능한 요소가 몇 개인지 나타내는 숫자를 돌려줘야 해요. 사용자가 직접 호출할 수도 있고, @foo[*-1]처럼 끝에서부터 인덱싱할 때 postcircumfix [ ]가 호출하기도 해요.
구현하지 않으면 타입이 Any의 기본 구현을 상속하는데, 정의된 호출자에 대해 항상 1을 돌려주기 때문에 원하는 게 아닐 가능성이 높아요. positional 타입에서 요소 수를 알 수 없다면, 조용히 잘못 동작하는 걸 피하려고 실패하거나 die하는 구현을 추가하세요.
method AT-POS
multi method AT-POS (::?CLASS:D: $index)
위치 $index의 요소를 돌려줘야 해요. postcircumfix [ ]가 보통 호출하는 게 이거예요.
요소가 (내장 Array 타입처럼) 가변적이길 원한다면, 읽을 때 요소 값으로 평가되고 할당될 때 갱신되는 항목 컨테이너 형태로 돌려줘야 해요. return-rw나 is rw 루틴 트레잇을 쓰는 걸 잊지 마세요. 예제를 보세요.
method EXISTS-POS
multi method EXISTS-POS (::?CLASS:D: $index)
위치 $index에 요소가 있는지 여부를 Bool로 돌려줘야 해요. @foo[42]:exists처럼 호출했을 때 postcircumfix [ ]가 호출하는 게 이거예요.
요소의 "존재"가 무엇을 뜻하는지는 타입에 달려 있어요.
구현하지 않으면 Any의 기본 구현을 상속하는데, 0에 대해 True를 그 외 인덱스에 대해 False를 돌려주기 때문에 원하는 게 아닐 가능성이 높아요. 요소 존재를 확인할 수 없는 타입이라면 조용히 잘못 동작하는 걸 피하려고 실패하거나 die하는 구현을 추가하세요.
method DELETE-POS
multi method DELETE-POS (::?CLASS:D: $index)
위치 $index의 요소를 삭제하고, 그 값이 무엇이었는지 돌려줘야 해요. @foo[42]:delete처럼 호출했을 때 postcircumfix [ ]가 호출하는 게 이거예요.
요소의 "삭제"가 무엇을 뜻하는지는 타입에 달려 있어요.
이 메서드 구현은 선택 사항이에요. 구현하지 않으면 이 타입의 객체에서 요소를 삭제하려는 사용자가 적절한 오류 메시지를 받아요.
method ASSIGN-POS
multi method ASSIGN-POS (::?CLASS:D: $index, $new)
위치 $index의 요소를 값 $new로 설정해야 해요. 구현은 전적으로 선택 사항이고, 구현하지 않으면 self.AT-POS($index) = $new가 대신 쓰여요. 구현한다면 같은 효과를 내도록 해야 해요.
이건 옵트인 성능 최적화로, @numbers[5] = "five" 같은 단순 할당이 (만들고 돌려줘야 할 비싼 컨테이너 객체가 있는) AT-POS를 호출하지 않고 동작하게 하려는 거예요.
ASSIGN-POS를 구현한다고 AT-POS를 가변(rw) 메서드로 만들 의무가 없어지는 건 아니에요. @numbers[5]++ 같은 덜 사소한 할당/수정은 여전히 AT-POS를 쓰니까요.
method BIND-POS
multi method BIND-POS (::?CLASS:D: $index, \new)
값이나 컨테이너 new를 위치 $index의 슬롯에 바인딩해서, 자연히 거기 있을 컨테이너를 바꿔치기해야 해요. 이렇게 쓸 때 호출되는 게 이거예요:
my $x = 10;
@numbers[5] := $x;
일반 Array 클래스는 복잡한 연결 데이터 구조를 만들 수 있게 이걸 지원하지만, 더 도메인 특화된 타입에는 의미가 없을 수 있으니 꼭 구현할 필요는 없어요. 구현하지 않으면 이 타입 객체의 positional 슬롯에 바인딩하려는 사용자가 적절한 오류 메시지를 받아요.
method STORE
method STORE (::?CLASS:D: \values, :$INITIALIZE)
이 메서드는 이 문법을 지원하고 싶을 때만 제공하면 돼요:
my @a is Foo = 1,2,3;
Positional 역할 구현을 바인딩하는 데 쓰이는 형태예요.
STORE는 객체를 (재)초기화할 값을 받아야 해요. 선택적 named 파라미터는 메서드가 객체에 처음 호출될 때 True 값을 담아요. 호출자를 돌려줘야 해요.
role Logger { method log( Str $msg) {…}}
class ConsoLogger does Logger { method log ( Str $msg ) { "❢ $msg".say }}
class DNA {
has $.chain;
has Logger $!logger;
submethod BUILD( :$chain, :$logger = ConsoLogger.new() ) {}
method STORE (Str $chain where {
/^^ <[ACGT]>+ $$ / and
.chars %% 3
},
:$INITIALIZE --> DNA) {
if ($INITIALIZE) {
$!logger = ConsoLogger.new();
$!logger.log( "Initialized" );
}
$!chain := $chain;
$!logger.log("Change value to $chain" );
self
}
method Str(::?CLASS:D:) { return $!chain.comb.rotor(3).map( *.join("")).join("|") }
};
my @string is DNA = 'GAATCC'; # OUTPUT: «❢ Initialized❢ Change value to GAATCC»
say ~@string; # OUTPUT: «GAA|TCC»
@string = 'ACGTCG'; # OUTPUT: «❢ Change value to ACGTCG»
say ~@string; # OUTPUT: «ACG|TCG»
이 코드는 $INITIALIZE의 값을 고려해요. 이 값은 is 문법으로 선언한 변수에 값을 처음 할당할 때만 True로 설정돼요. 예를 들어 이 경우처럼 주입된 의존성을 초기화해야 할 수 있죠. STORE 메서드는 모든 경우에 self 변수를 설정하고 돌려줘야 해요. 다만 첫 번째 경우에만 이 예제에서 쓰는 logger를 초기화하면 돼요.
INITIALIZE 플래그의 존재는 불변 데이터 구조를 만드는 데도 쓸 수 있어요:
class A {
has @.foo handles <Str gist raku>;
multi method STORE(*@!foo, :$INITIALIZE!) { self }
multi method STORE(|) { die "Immutable" }
}
my @a is A = 1,2,3,4;
say @a; # OUTPUT: «[1,2,3,4]»
@a = 4,5,6,7; # dies: Immutable
Associative 첨자화를 위해 구현할 메서드
postcircumfix { }로 키 기반 첨자화가 동작하게 하려면 최소한 AT-KEY와 EXISTS-KEY를 구현해야 해요 (그리고 아래에서 자세히 나오는 것들을 선택적으로).
method AT-KEY
multi method AT-KEY (::?CLASS:D: $key)
$key에 연관된 요소를 돌려줘야 해요. postcircumfix { }가 보통 호출하는 게 이거예요.
요소가 (내장 Hash 타입처럼) 가변적이길 원한다면, 읽을 때 요소 값으로 평가되고 할당될 때 갱신되는 항목 컨테이너 형태로 돌려줘야 해요. return-rw나 is rw 루틴 트레잇을 쓰는 걸 잊지 마세요. 예제를 보세요.
반면 컬렉션을 읽기 전용으로 만들고 싶다면 컨테이너가 아닌 값을 그냥 돌려줘도 돼요.
method EXISTS-KEY
multi method EXISTS-KEY (::?CLASS:D: $key)
$key에 연관된 요소가 있는지 여부를 Bool로 돌려줘야 해요. %foo<aa>:exists처럼 호출했을 때 postcircumfix { }가 호출하는 게 이거예요.
요소의 "존재"가 무엇을 뜻하는지는 타입에 달려 있어요.
구현하지 않으면 Any의 기본 구현을 상속하는데, 항상 False를 돌려주기 때문에 원하는 게 아닐 가능성이 높아요. 요소 존재를 확인할 수 없는 타입이라면 조용히 잘못 동작하는 걸 피하려고 실패하거나 die하는 구현을 추가하세요.
method DELETE-KEY
multi method DELETE-KEY (::?CLASS:D: $key)
$key에 연관된 요소를 삭제하고 그 값이 무엇이었는지 돌려줘야 해요. %foo<aa>:delete처럼 호출했을 때 postcircumfix { }가 호출하는 게 이거예요.
요소의 "삭제"가 무엇을 뜻하는지는 타입에 달려 있지만, 보통 그 키에 대해 EXISTS-KEY가 False가 되게 해야 해요.
이 메서드 구현은 선택 사항이에요. 구현하지 않으면 이 타입의 객체에서 요소를 삭제하려는 사용자가 적절한 오류 메시지를 받아요.
method ASSIGN-KEY
multi method ASSIGN-KEY (::?CLASS:D: $key, $new)
$key에 연관된 요소를 값 $new로 설정해야 해요. 구현은 전적으로 선택 사항이고, 구현하지 않으면 self.AT-KEY($key) = $new가 대신 쓰여요. 구현한다면 같은 효과를 내도록 해야 해요.
이건 옵트인 성능 최적화로, %age<Claire> = 29 같은 단순 할당이 (만들고 돌려줘야 할 비싼 컨테이너 객체가 있는) AT-KEY를 호출하지 않고 동작하게 하려는 거예요.
ASSIGN-KEY를 구현한다고 AT-KEY를 rw 메서드로 만들 의무가 없어지는 건 아니에요. %age<Claire>++ 같은 덜 사소한 수정은 여전히 AT-KEY를 쓰니까요.
method BIND-KEY
multi method BIND-KEY (::?CLASS:D: $key, \new)
값이나 컨테이너 new를 $key에 연관된 슬롯에 바인딩해서, 자연히 거기 있을 컨테이너를 바꿔치기해야 해요. 이렇게 쓸 때 호출되는 게 이거예요:
my $x = 10;
%age<Claire> := $x;
일반 Hash 클래스는 복잡한 연결 데이터 구조를 만들 수 있게 이걸 지원하지만, 더 도메인 특화된 타입에는 의미가 없을 수 있으니 꼭 구현할 필요는 없어요. 구현하지 않으면 이 타입 객체의 associative 슬롯에 바인딩하려는 사용자가 적절한 오류 메시지를 받아요.
method STORE
method STORE (::?CLASS:D: \values, :$INITIALIZE)
이 메서드는 이 문법을 지원하고 싶을 때만 제공하면 돼요:
my %h is Foo = a => 42, b => 666;
Associative 역할 구현을 바인딩하는 데 쓰이는 형태예요.
객체를 (재)초기화할 값을 받아야 하는데, Pair들이거나 별도의 키/값 쌍일 수 있어요. 선택적 named 파라미터는 메서드가 객체에 처음 호출될 때 True 값을 담아요. 호출자를 돌려줘야 해요.