Hash
Hash
키로 값을 찾아보는 연관 데이터는 아주 흔하게 쓰여요. Raku에서 그 기본 타입이 바로 Hash예요. 다른 언어에서는 딕셔너리(dict)나 객체(object), 해시 맵(hash map)이라고 부르는 구조죠.
본문
Hash는 변경 가능한(mutable) Map이에요. Map을 상속하면서 Associative를 구현해서, 키로 값을 찾아보는 연관 첨자(associative subscripting)를 지원하죠.
class Hash is Map { }
Hash는 % 시길(sigil)을 가진 변수의 기본 타입이에요. 해시는 키에서 값으로 가는 변경 가능한 매핑이고, Python의 dict, JavaScript의 object, Java의 Hash Map 같은 것이라고 보면 돼요.
기본 사용법부터 볼게요.
# initialization with pairs:
my %capitals = Spain => 'Madrid', 'United States' => 'Washington DC';
# adding another pair:
%capitals{'Zimbabwe'} = 'Harare';
# accessing a value by key:
my $country = 'Spain';
say "The capital of $country is %capitals{$country}";
# getting all keys:
say "I know the capitals of these countries: ", %capitals.keys.sort.join(', ');
# check if a key is in a hash:
if %capitals{'Europe'}:exists {
# not executed
}
# iterating over keys and values (unordered):
for %capitals.kv -> $country, $capital {
say "$capital is the capital of $country";
}
해시의 순서는 매 호출마다 무작위임이 보장되지만, 그래도 연속된 .keys와 .values 호출은 항상 같은 순서를 반환하도록 보장돼요.
my %orig = :1a, :2b; my %new = :5b, :6c;
%orig{ %new.keys } = %new.values;
say %orig.raku; # OUTPUT: «{:a(1), :b(5), :c(6)}»
이 경우 b는 항상 5와, c는 항상 6과 연결돼요. 두 번 연속 keys를 호출해도 그 순서가 다를 뿐이지, 어느 프로그램 실행에서든 각각을 반복해서 호출하면 항상 같은 순서를 돌려줘요.
해시를 선언하는 여러 방법은 해시 리터럴 문서에서 확인할 수 있어요. 게다가 다음 규칙만 지킨다면 중괄호로도 선언할 수 있어요.
- 빈 중괄호는 항상 빈 해시를 선언해요.
$_에 대한 참조(암시적이어도)가 있으면 대신 블록(block)을 선언해요.- 첫 번째 요소가
Pair이거나%가 붙은 변수면 해시를 선언해요.
given 3 { say WHAT {3 => 4, :b} }; # OUTPUT: «(Hash)»
given 3 { say WHAT {3 => 4, :b($_)} }; # OUTPUT: «(Block)»
given 3 { say WHAT {3 => 4, :b(.Num)} };# OUTPUT: «(Block)»
say { 'a',:b(3), 'c' }.^name; # OUTPUT: «Block»
마지막에서 두 개의 경우는 토픽 변수 $_가 있을 때 Block이 생성되는 예시예요. 마지막 경우는 해시를 생성하는 세 번째 기준을 충족하지 않아서 Block이 생성됩니다.
괄호나 대괄호 앞에 %가 있으면, 요소들을 서로 짝지을 수 있는 한 Hash가 생성돼요.
say %( 'a', 3, :b(3), 'c', 3 ).^name; # OUTPUT: «Hash»
이 해시의 요소들은 :b(3) Pair의 양쪽으로 짝지을 수 있어요.
say %(«a b c 1 2 3»).^name; # OUTPUT: «Hash»
빈 해시는 빈 중괄호로 초기화하거나, 6.d부터는 %()로 초기화할 수 있어요.
say %().^name; # OUTPUT: «Hash»
say {}.^name; # OUTPUT: «Hash»
해시는 타입으로 매개 변수화(parameterize)할 수 있어요. 키의 타입을 이렇게 바꿀 수 있어요.
my %next-prime{Int} = 2 => 3, 3 => 5, 5 => 7, 7 => 11, 11 => 13;
값의 타입은 기본적으로 Mu인데, 다른 타입으로 제약할 수 있어요.
my Array %lists;
두 기능을 결합할 수도 있어요.
my Array %next-primes{Int} = 2 => [3, 5], 11 => [13, 17];
메서드
method classify-list
multi method classify-list(&mapper, *@list, :&as --> Hash:D)
multi method classify-list(%mapper, *@list, :&as --> Hash:D)
multi method classify-list(@mapper, *@list, :&as --> Hash:D)
주어진 mapper로 (비어 있을 수도 있는) @list 값들을 분류해서 Hash를 채워요. :&as Callable을 주면 값을 그걸로 바꿀 수도 있어요. @list는 lazy일 수 없어요.
mapper는 단일 인자를 받는 Callable이거나, Associative이거나, Iterable일 수 있어요. 이 Callable은 항목당 정확히 한 번만 호출되는 게 보장돼요. Associative와 Iterable mapper에서는 @list의 값이 각각 mapper 값의 키와 인덱스를 나타내요. Callable mapper는 @list의 각 항목마다 한 번씩 실행되고, 그 항목을 인자로 받아 반환 값이 mapper의 값으로 쓰여요.
단순 분류 (Simple classification)
단순 분류 모드에서는 각 mapper 값이 Iterable이 아닌 어떤 것이고, @list의 항목을 분류할 키를 나타내요.
say % .classify-list: { $_ %% 2 ?? 'even' !! 'odd' }, ^10;
# OUTPUT: «{even => [0 2 4 6 8], odd => [1 3 5 7 9]}»
my @mapper = <zero one two three four five>;
my %hash = foo => 'bar';
say %hash.classify-list: @mapper, 1, 2, 3, 4, 4;
# OUTPUT: «{foo => bar, four => [4 4], one => [1], three => [3], two => [2]}»
mapper 값은 @list의 항목이 push될 Hash의 키로 쓰여요. 항목을 한 번에 여러 범주로 분류하고 싶다면 .categorize-list를 보세요.
다단계 분류 (Multi-level classification)
다단계 분류 모드에서는 각 mapper 값이 @list의 항목을 분류할 해시 키들의 트리를 나타내는 Iterable이에요.
say % .classify-list: {
[
(.is-prime ?? 'prime' !! 'non-prime'),
($_ %% 2 ?? 'even' !! 'odd' ),
]
}, ^10;
# OUTPUT:
# {
# non-prime => {
# even => [0 4 6 8],
# odd => [1 9]
# },
# prime => {
# even => [2],
# odd => [3 5 7]
# }
# }
Callable이 아니라 Iterable을 쓰는 경우, 각 Iterable은 같은 개수의 요소를 가져야 해요. 그렇지 않으면 메서드가 예외를 던집니다. 이 제약은 어떤 값의 분류에서 같은 키가 잎(leaf)인데 다른 값의 분류에서는 노드(node)가 되는 충돌을 피하기 위해 존재해요.
my @mapper = [['1a','1b','1c'],['2a','2b','2c'],['3a','3b','3c']];
say % .classify-list: @mapper, 1,2,1,1,2,0;
# OUTPUT: «{1a => {1b => {1c => [0]}}, 2a => {2b => {2c => [1 1 1]}}, 3a => {3b => {3c => [2 2]}}}»
배열의 각 요소는 트리의 서로 다른 깊이를 나타내요. 매핑되는 리스트의 요소는 인덱스로, mapper 배열의 요소는 서로 다른 깊이의 키로 쓰여요. 그래서 0은 첫 번째 하위 배열을 고르고, 이후 깊이는 그 하위 배열의 나머지 요소를 지나며 쌓아 올려져요.
my @mapper = [['1a','1b'],['2a','2b'],['3a','3b']];
say % .classify-list: @mapper, 1,0,1,1,1,0,2;
# OUTPUT: «{1a => {1b => [0 0]}, 2a => {2b => [1 1 1 1]}, 3a => {3b => [2]}}»
6.d 버전부터 서로 다른 크기의 Iterable을 쓰려고 하면 오류가 발생해요.
my @mapper = [<1a 1b>, <2a 2b 2fail>];
say % .classify-list: @mapper, 1,0,1,1,1,0;
# OUTPUT: «mapper on classify-list computed to an item with different number
# of elements in it than previous items, which cannot be used because all
# values need to have the same number of elements. Mixed-level classification
# is not supported. in block <unit>…»
:&as 값 수정자 (value modifier)
:&as Callable 인자를 지정하면 @list의 각 항목마다 한 번씩 호출되고, 값을 인자로 받아 그 반환 값이 원래 @list 항목 대신 쓰여요.
say % .classify-list: :as{"Value is $_"}, { $_ %% 2 ?? 'even' !! 'odd' }, ^5;
# OUTPUT (slightly altered manually, for clarity):
# {
# even => ['Value is 0', 'Value is 2', 'Value is 4'],
# odd => ['Value is 1', 'Value is 3']
# }
method categorize-list
multi method categorize-list(&mapper, *@list, :&as --> Hash:D)
multi method categorize-list(%mapper, *@list, :&as --> Hash:D)
multi method categorize-list(@mapper, *@list, :&as --> Hash:D)
주어진 mapper로 (비어 있을 수도 있는) @list 값들을 분류해서 Hash를 채워요. :&as Callable을 주면 값을 바꿀 수도 있어요. @list는 lazy일 수 없어요.
mapper는 단일 인자를 받는 Callable이거나, Associative이거나, Iterable일 수 있어요. Associative와 Iterable mapper에서는 @list의 값이 각각 mapper 값의 키와 인덱스를 나타내요. Callable mapper는 @list의 각 항목마다 한 번씩 실행되고, 그 항목을 인자로 받아 반환 값이 mapper의 값으로 쓰여요.
단순 분류 (Simple categorization)
mapper 값은 값을 넣을 범주들을 나타내는, Iterable이 아닌 값들의 (비어 있을 수도 있는) 리스트로 예상돼요.
say % .categorize-list: {
gather {
take 'prime' if .is-prime;
take 'largish' if $_ > 5;
take $_ %% 2 ?? 'even' !! 'odd';
}
}, ^10;
# OUTPUT:
# {
# prime => [2 3 5 7]
# even => [0 2 4 6 8],
# odd => [1 3 5 7 9],
# largish => [6 7 8 9],
# }
6이나 7 같은 일부 항목이 여러 범주에 들어가는 것을 볼 수 있어요.
다단계 분류 (Multi-level categorization)
다단계 분류에서는 mapper가 만들어낸 범주들이 Iterable이에요. 그리고 분류는 각 범주에 대해 분류의 중첩 해시를 만들어 내는 classify의 특징을 결합해요.
say % .categorize-list: {
[
$_ > 5 ?? 'largish' !! 'smallish',
.is-prime ?? 'prime' !! 'non-prime',
],
}, ^10;
# OUTPUT:
# {
# largish => {
# non-prime => [6 8 9],
# prime => [7]
# },
# smallish => {
# non-prime => [0 1 4],
# prime => [2 3 5]
# }
# }
위 스니펫의 mapper는 항목이 두 개인 Array를 담은 단일 항목 리스트(뒤에 붙은 중요한 trailing comma를 주목하세요)를 만들어요. 배열의 첫 번째 항목은 첫 번째 분류 깊이를 나타내요. 즉 루틴이 만드는 largish/smallish 범주죠. 두 번째 항목은 이후 분류 깊이를 나타내고, 여기서는 각 범주 안에서 prime/non-prime으로 나누는 거예요.
참고: 모든 Iterable 범주는 같은 개수의 요소를 가져야 해요. 그렇지 않으면 메서드가 예외를 던집니다. 어떤 값의 분류에서 같은 키가 잎인데 다른 값의 분류에서는 노드가 되는 충돌을 피하기 위해서예요.
:&as 값 수정자 (value modifier)
:&as Callable 인자를 지정하면 @list의 각 항목마다 한 번씩 호출되고, 값을 인자로 받아 그 반환 값이 원래 @list 항목 대신 쓰여요.
say % .categorize-list: :as{"Value is $_"}, { $_ %% 2 ?? 'even' !! 'odd' }, ^5;
# OUTPUT (slightly altered manually, for clarity):
# {
# even => ['Value is 0', 'Value is 2', 'Value is 4'],
# odd => ['Value is 1', 'Value is 3']
# }
method push
method push(Hash:D: +new)
new 요소들을 해시 할당과 같은 의미로 해시에 추가해요. 단 세 가지 예외가 있어요.
- 해시를 먼저 비우지 않아요. 즉 기존 pair를 삭제하지 않아요.
- 키가 이미 해시에 있고 그에 해당하는 값이
Array라면, 그 배열에 새 값을 push해요(교체 대신). - 키가 이미 해시에 있고 그에 해당하는 값이
Array가 아니라면, 기존 값과 새 값 모두를 그 자리에 배열 안에 넣어요.
예를 들면,
my %h = a => 1;
%h.push: (a => 1); # a => [1,1]
%h.push: (a => 1) xx 3 ; # a => [1,1,1,1,1]
%h.push: (b => 3); # a => [1,1,1,1,1], b => 3
%h.push('c' => 4); # a => [1,1,1,1,1], b => 3, c => 4
push %h, 'd' => 5; # a => [1,1,1,1,1], b => 3, c => 4, d => 5
인자 목록의 리터럴 pair는 이름 인자로 해석될 수 있어서 Hash에 들어가지 않을 수 있다는 점에 주의하세요.
my %h .= push(e => 6);
say %h.raku; # OUTPUT: «{}»
이런 실수를 잡으려면 해당 subroutine을 쓰세요.
push my %h, f => 7;
CATCH { default { put .message } };
# OUTPUT: «Unexpected named argument 'f' passed»
또한 push는 해시 초기화 중 할당의 대체로 아주 유용하게 쓰일 수 있어요. 역 인덱스(inverted index)의 경우를 예로 들어 볼게요.
my %wc = 'hash' => 323, 'pair' => 322, 'pipe' => 323;
(my %inv).push: %wc.invert;
say %inv; # OUTPUT: «{322 => pair, 323 => [pipe hash]}»
이런 초기화는 다음과 같이 쓸 수도 있어요.
my %wc = 'hash' => 323, 'pair' => 322, 'pipe' => 323;
my %inv .= push: %wc.invert;
참고: append와 비교하면, push는 주어진 값을 그대로 추가하는 반면 append는 slip해서 넣어요.
my %ha = :a[42, ]; %ha.push: "a" => <a b c a>;
say %ha; # OUTPUT: «{a => [42 (a b c a)]}»
my %hb = :a[42, ]; %hb.append: "a" => <a b c a>;
say %hb; # OUTPUT: «{a => [42 a b c a]}»
method append
method append(+@values)
제공된 Pair들 또는 짝수 크기 리스트를 해시에 추가해요. 키가 이미 있으면 기존 값을 Array로 바꾸고 그 배열에 새 값을 push해요. 짝수 크기 리스트와 Pair 리스트를 섞을 수 없다는 점, 그리고 맨몸의 Pair나 콜론 pair는 .append의 이름 인자로 취급된다는 점을 주의하세요.
my %h = a => 1;
%h.append('b', 2, 'c', 3);
%h.append( %(d => 4) );
say %h;
# OUTPUT: «{a => 1, b => 2, c => 3, d => 4}»
%h.append('a', 2);
# OUTPUT: «{a => [1 2], b => 2, c => 3, d => 4}»
참고: push와 비교하면, append는 주어진 값을 slip해서 넣는 반면 push는 그대로 추가해요.
my %hb = :a[42, ]; %hb.append: "a" => <a b c a>;
say %hb; # OUTPUT: «{a => [42 a b c a]}»
my %ha = :a[42, ]; %ha.push: "a" => <a b c a>;
say %ha; # OUTPUT: «{a => [42 (a b c a)]}»
method default
method default(Hash:D:)
invocant의 기본값, 즉 없는 키로 Hash의 요소에 접근할 때 돌려주는 값을 반환해요. is default trait으로 기본값을 선언하지 않은 해시라면 이 메서드는 타입 객체 (Any)를 돌려줘요.
my %h1 = 'apples' => 3, 'oranges' => 7;
say %h1.default; # OUTPUT: «(Any)»
say %h1{'bananas'}; # OUTPUT: «(Any)»
my %h2 is default(1) = 'apples' => 3, 'oranges' => 7;
say %h2.default; # OUTPUT: «1»
say %h2{'apples'} + %h2{'bananas'}; # OUTPUT: «4»
method keyof
method keyof()
invocant의 키에 대한 타입 제약을 돌려줘요. 일반 해시에서는 강제 변환 타입 (Str(Any))을 돌려주고, 비문자열 키(object hash) 해시에서는 Hash 선언에 쓰인 타입을 돌려줘요.
my %h1 = 'apples' => 3, 'oranges' => 7; # (no key type specified)
say %h1.keyof; # OUTPUT: «(Str(Any))»
my %h2{Str} = 'oranges' => 7; # (keys must be of type Str)
say %h2.keyof; # OUTPUT: «(Str)»
%h2{3} = 'apples'; # throws exception
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::TypeCheck::Binding: Type check failed in binding to key; expected Str but got Int (3)»
my %h3{Int}; # (this time, keys must be of type Int)
%h3{42} = 4096;
say %h3.keyof; # OUTPUT: «(Int)»
method of
method of(Hash:D:)
invocant의 값에 대한 타입 제약을 돌려줘요. 기본적으로, 즉 선언 중 타입 제약을 주지 않았다면 (Mu)를 돌려줘요.
my %h1 = 'apples' => 3, 'oranges' => 7; # (no type constraint specified)
say %h1.of; # OUTPUT: «(Mu)»
my Int %h2 = 'oranges' => 7; # (values must be of type Int)
say %h2.of; # OUTPUT: «(Int)»
routine dynamic
method dynamic(--> Bool:D)
invocant이 is dynamic trait으로 선언됐다면 True를 돌려줘요.
my %a;
say %a.dynamic; # OUTPUT: «False»
my %b is dynamic;
say %b.dynamic; # OUTPUT: «True»
* twigil로 변수를 선언하면 is dynamic이 암시돼요.
my %*b;
say %*b.dynamic; # OUTPUT: «True»
Scalar의 경우에는 정확한 정보를 얻으려면 VAR 메서드를 써야 한다는 점을 주의하세요.
my $s is dynamic = %('apples' => 5);
say $s.dynamic; # OUTPUT: «False» (wrong, don't do this)
say $s.VAR.dynamic; # OUTPUT: «True» (correct approach)
첨자 부사 (Subscript Adverbs)
몇몇 메서드는 첨자(subscript)의 부사(adverb)로 구현돼요. 자세한 내용은 operators 문서를 참고하세요.
:exists
:exists 부사는 키가 해시에 있으면 Bool::True를 돌려줘요. 키를 두 개 이상 주면 Bool들의 List를 돌려줘요.
my %h = a => 1, b => 2;
say %h<a>:exists; # OUTPUT: «True»
say %h<a b>:exists; # OUTPUT: «(True True)»
:delete
:delete를 써서 Hash에서 Pair를 제거할 수 있어요. 값은 항상 돌려주지만, delete가 참일 때만 제거가 일어나요.
my %h = a => 1;
say %h; # OUTPUT: «{a => 1}»
say %h.elems; # OUTPUT: «1»
%h<a>:delete;
say %h; # OUTPUT: «{}»
say %h.elems; # OUTPUT: «0»
:p
:p 부사는 값만 돌려주는 대신 Pair 또는 Pair들의 List를 돌려줘요.
my %h = a => 1, b => 2;
say %h<a>:p; # OUTPUT: «a => 1»
say %h<a b>:p; # OUTPUT: «(a => 1 b=> 2)»
:v와 :k
:v와 :k 부사는 값 또는 키, 혹은 그 목록을 돌려줘요.
my %h = a => 1, b => 2;
say %h<a>:k; # OUTPUT: «a»
say %h<a b>:k; # OUTPUT: «(a b)»
:kv 부사는 키와 값의 리스트를 돌려줘요.
my %h = a => 1, b => 2, c => 3;
say %h<a c>:kv; # OUTPUT: «(a 1 c 3)»
해시에 대해 아무것도 모른 채 부사를 쓰려면 빈 꺾쇠 괄호를 쓸 수도 있어요. 그러면 모든 키와 값이 나열됩니다.
my %h1 = a => 1;
my %h2 = a => 1, b => 2;
say %h1<>:k; # OUTPUT: «(a)»
say %h1<>:v; # OUTPUT: «(1)»
say %h2<>:k; # OUTPUT: «(a b)»
say %h2<>:v; # OUTPUT: «(1 2)»