Hash

Hash

키로 값을 찾아보는 연관 데이터는 아주 흔하게 쓰여요. Raku에서 그 기본 타입이 바로 Hash예요. 다른 언어에서는 딕셔너리(dict)나 객체(object), 해시 맵(hash map)이라고 부르는 구조죠.

출처: Raku 공식 문서 — Hash

본문

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은 항목당 정확히 한 번만 호출되는 게 보장돼요. AssociativeIterable 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의 항목이 pushHash의 키로 쓰여요. 항목을 한 번에 여러 범주로 분류하고 싶다면 .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일 수 있어요. AssociativeIterable 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는 주어진 값을 그대로 추가하는 반면 appendslip해서 넣어요.

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)␤»