리스트, 시퀀스, 배열

리스트, 시퀀스, 배열 (Lists, sequences, and arrays)

리스트는 컴퓨터가 존재하기 이전부터 컴퓨팅의 중심이었어요. 그 오랜 세월 동안 그 세부 사항에는 온갖 난관이 숨어들었죠. Raku에서 리스트는 실제로 설계하기 가장 어려운 부분 중 하나였는데, 끈기와 인내 끝에 Raku는 이를 우아하게 다루는 체계를 갖추게 됐어요. 이 문서에서 Raku의 리스트(과 그 친척인 시퀀스·배열)를 하나씩 풀어볼게요.

출처: Raku Documentation — Lists, sequences, and arrays

본문

리터럴 리스트 (Literal lists)

리터럴 리스트는 쉼표와 세미콜론으로 만들지, 괄호로는 만들지 않아요 (괄호는 그룹만 지을 뿐이에요). 그래서 이렇게 돼요.

1, 2;                # This is two-element list
our $list = (1, 2);  # This is also a List, in parentheses
$list = (1; 2);      # same List (see below)
$list = (1);         # This is not a List, just a 1 in parentheses
$list = (1,);        # This is a one-element List

예외가 하나 있는데, 빈 리스트는 괄호 한 쌍으로 만들어요.

();          # This is an empty List
(,);         # This is a syntax error

꼬리에 붙는 쉼표(hanging comma, trailing comma)는 리스트의 시작과 끝이 분명하다면 문제없어요. 코드를 쉽게 편집하려고 마음껏 써도 좋아요.

괄호는 리스트의 시작과 끝을 표시하는 데 쓰일 수 있어요.

(1, 2), (1, 2); # This is a list of two lists.

리스트의 리스트는 쉼표와 세미콜론을 조합해 만들 수도 있어요. 이를 **다차원 문법(multi-dimensional syntax)**이라고도 해요. 다차원 배열을 인덱싱할 때 가장 많이 쓰이기 때문이에요.

say so (1,2; 3,4) eqv ((1,2), (3,4));
# OUTPUT: «True␤»
say so (1,2; 3,4;) eqv ((1,2), (3,4));
# OUTPUT: «True␤»
say so ("foo";) eqv ("foo") eqv (("foo")); # not a list
# OUTPUT: «True␤»

쉼표와 달리, 꼬리에 붙은 세미콜론은 리터럴에서 다차원 리스트를 만들지 않아요. 다만 이 동작은 대부분의 인자 목록에서는 달라진다는 점을 알아두세요. 정확한 동작은 함수에 따라 다르지만, 보통은 이렇게 돼요.

say('foo';);   # a list with one element and the empty list
# OUTPUT: «(foo)()␤»
say(('foo';)); # no list, just the string "foo"
# OUTPUT: «foo␤»

세미콜론은 문장 종결자(statement terminator)도 겸하기 때문에, 최상위 레벨에서 쓰면 리터럴 리스트를 끝내는 대신 **문장 목록(statement list)**을 만들어요. 괄호 안에 문장 목록을 만들고 싶다면 괄호 앞에 시길을 써요.

say so (42) eqv $(my $a = 42; $a;);
# OUTPUT: «True␤»
say so (42,42) eqv (my $a = 42; $a;);
# OUTPUT: «True␤»

리스트의 개별 요소는 첨자(subscript)로 꺼낼 수 있어요. 리스트의 첫 번째 요소의 인덱스는 0이에요.

say (1, 2)[0];  # says 1
say (1, 2)[1];  # says 2
say (1, 2)[2];  # says Nil

끝에서부터 거꾸로 세는 것은 Whatever 별표(Whatever star)로 해요 (자세한 내용은 Subscripts 문서 참고).

say (1, 2)[*-1]; # says 2
say (1, 2)[-1];  # Error

중첩(다차원) 리스트의 요소에 접근할 때는 서로 다른 차원의 인덱스를 세미콜론으로 구분해요.

say ((<a b>,<c d>),(<e f>,<g h>))[1;0;1]; # says "f"

한 번에 여러 요소를 꺼내려면 인덱스의 리스트나 범위를 넘기면 돼요. 이 내용은 "Slice indexing context"와 "Range as slice" 절에서 설명해요.

@ 시길 (The @ sigil)

이름에 @ 시길을 가진 Raku 변수는 어떤 종류의 리스트 같은 객체를 담을 것으로 기대돼요. 다른 변수도 그런 객체를 담을 수 있지만, @ 시길 변수는 항상 담아야 하고 또 그렇게 행동할 것으로 기대돼요.

기본적으로 List@ 시길 변수에 할당하면 Array를 만들어요. 이에 대해서는 아래에서 설명할게요. 반면 @ 시길 변수로 List 객체를 직접 가리키고 싶다면, :=로 바인딩(binding)을 쓰면 돼요.

my @a := 1, 2, 3;

@ 시길 변수가 리스트처럼 행동하는 방식 중 하나는, 항상 위치 첨자(positional subscripting)를 지원한다는 거예요. @ 시길 값에 바인딩된 어떤 것이든 Positional 롤을 지원해야 해요. 이 롤은 아래 같은 코드가 실패한다는 것을 보장해요.

my @a := 1; # Type check failed in binding; expected Positional but got Int

리스트 컨테이너 비우기 (Reset a list container)

Positional 컨테이너에서 모든 요소를 제거하려면 그 컨테이너에 Empty, 빈 리스트 () 또는 빈 리스트의 Slip을 할당하면 돼요.

my @a = 1, 2, 3;
@a = ();
@a = Empty;
@a = |();

반복 (Iteration)

모든 리스트는 반복될 수 있어요. 각 요소를 순서대로 꺼내 마지막 요소에서 멈추는 것이죠.

for 1, 2, 3 { .say }  # OUTPUT: «1␤2␤3␤»

단일 인자 규칙 (Single Argument Rule)

단일 인자 규칙for 같은 반복자에 전달되는 인자들의 집합을 여러 인자 대신 하나의 인자로 취급하는 규칙이에요. 즉 some-iterator( a, b, c, ...)는 항상 some-iterator( list-or-array(a, b, c))로 취급돼요. 다음 예시에서 그걸 볼 수 있어요.

my @list = [ (1, 2, 3),
             (1, 2, ),
             [<a b c>, <d e f>],
             [[1]] ];

for @list -> @element {
    say "{@element} → {@element.^name}";
    for @element -> $sub-element {
        say $sub-element;
    }
}
# OUTPUT:
#1 2 3 → List
#1
#2
#3
#1 2 → List
#1
#2
#a b c d e f → Array
#(a b c)
#(d e f)
#1 → Array
#1

for가 받는 것은 하나의 인자이므로, 그것은 반복할 요소들의 리스트로 취급돼요. 요령은 이래요. 쉼표가 있으면 그 앞에 오는 것은 요소이고, 그래서 만들어진 리스트가 단일 요소가 돼요. 이 예시에서 우리가 반복하는 Array의 세 번째 요소인, 쉼표로 구분된 두 배열의 경우가 그렇게 돼요. 일반적으로, 위에 링크된 글을 인용하면, 단일 인자 규칙은 ... 프로그래머가 기대하는 대로 동작하게 만들어요.

이 규칙은 "반복자의 인자는 펼쳐지지(flatten) 않고, 디컨테이너화되지(de-containerize) 않으며, 인자가 가진 형태와 무관하게 하나의 인자로 다뤄진다"라고 말하는 것과 동등해요.

my @a = 1,2; .say for @a, |@a;     # OUTPUT: «[1 2]␤1␤2␤»
my @a = 1,2; .say for $[@a, |@a ]; # OUTPUT: «[[1 2] 1 2]␤»

두 번째 경우에는 우리가 배열을 itemize 했으므로, 단일 인자가 단일 요소가 돼요. Synopsis에 언급된 단일 인자 규칙의 예외가 하나 있는데, 단일 요소를 가진 리스트나 배열은 펼쳐진다는 거예요.

my @a = 1,2; .say for [[@a ]];     # OUTPUT: «1␤2␤»

꼬리에 쉼표를 쓴 경우 결과가 조금 놀라울 수 있어요.

my @a = 1,2; .say for @a,;         # OUTPUT: «[1 2]␤»

하지만 쉼표 연산자는 사실 단일 요소를 가진 고차(high-order) 리스트를 만드는 거고, 그 요소도 역시 리스트예요. 그리 놀랄 일은 아니죠. 단일 요소를 가졌기 때문에, 그런 고차 리스트도 위처럼 펼쳐져요.

my @a = 1,2; .say for [@a,];       # OUTPUT: «[1 2]␤»

요소 존재 검사 (Testing for elements)

ListArray에 요소가 있는지 검사하려면 "is element of" Set 연산자를 쓸 수 있어요.

my @a = <foo bar buzz>;
say 'bar' (elem) @a;    # OUTPUT: «True␤»
say 'bar' ∈ @a;         # same, using unicode version of operator

이것은 아래와 동등해요.

'bar' (elem) @a.Set;    # convert the array to a Set first

다만, 가능하다면 실제로 변환을 하지는 않아요.

기본적으로 === 중위 연산자로 그 값을 배열의 각 요소와 비교해요. 값을 다른 방식으로 비교하고 싶다면 아마 first를 쓰는 게 좋아요.

시퀀스 (Sequences)

모든 리스트가 요소로 가득 차서 태어나는 건 아니에요. 어떤 리스트는 요청받은 만큼만 요소를 만들어내요. 이런 것을 **시퀀스(sequence)**라고 부르고, Seq 타입이에요. 실제로 루프는 Seq를 반환해요.

(loop { 42.say })[2]  # OUTPUT: «42␤42␤42␤»

그래서 Raku에서 무한 리스트를 가져도 괜찮아요. 단지 모든 요소를 요청하지 않으면 돼요. 어떤 경우에는 길이를 묻는 것도 피하고 싶을 수 있어요. Raku는 시퀀스가 무한하다는 걸 알면 Inf를 반환하려 하지만, 항상 알 수는 없어요.

이런 리스트는 ... 연산자로 만들 수 있어요. 다양항 생성 표현식으로 느긋한(lazy) 리스트를 만들어내는 연산자예요.

Seq 클래스는 일부 위치 첨자를 제공하지만 Positional의 완전한 인터페이스는 제공하지 않아요. 그래서 @ 시길 변수를 Seq에 바인딩할 수 없고, 시도하면 오류가 나요.

my @s := <a b c>.Seq; CATCH { default { say .^name, ' ', .Str } }
# OUTPUT: «X::TypeCheck::Binding Type check failed in binding; expected Positional but got Seq ($(("a", "b","c").Seq))␤»

이유는 Seq가 값을 사용한 후에 그 값을 보관하지 않기 때문이에요. 아주 긴 시퀀스를 다룰 때 유용한 동작이에요. 값을 사용한 후 버려서 프로그램이 메모리를 가득 채우지 않게 하고 싶으니까요. 예를 들어 백만 줄짜리 파일을 처리할 때처럼요.

for 'filename'.IO.lines -> $line {
    do-something-with($line);
}

명시적으로 줄을 어딘가에 저장하지 않는 한, 파일의 전체 내용이 메모리에 남아 있지 않을 거라고 확신할 수 있어요.

반대로, 어떤 경우에는 이전 값을 보관하고 싶을 수도 있어요. SeqList 안에 숨길 수 있는데, 여전히 lazy하긴 하지만 이전 값은 기억해요. .list 메서드를 호출하면 돼요. 이 ListPositional을 완전히 지원하므로, @ 시길 변수에 직접 바인딩할 수 있어요.

my @s := (loop { 42.say }).list;
@s[2]; # says 42 three times
@s[1]; # does not say anything
@s[4]; # says 42 two more times

참조를 어떻게 처리하고 싶은지에 따라 .list 대신 .cache 메서드를 쓸 수도 있어요. 자세한 내용은 Seq 문서를 참고하세요.

.iterator 사용하기 (Using .iterator)

모든 리스트는 Iterator 롤을 믹스인하고, 그 결과 리스트를 더 세밀하게 제어할 수 있는 .iterator 메서드를 가져요. 이렇게 쓸 수 있어요.

my @multiples-of-five = 0,5,10 … 500;
my $odd-iterator = @multiples-of-five.iterator;
my $odd;
repeat {
    $odd-iterator.skip-one;
    $odd = $odd-iterator.pull-one;
    say "→ $odd";
} until $odd.Str eq IterationEnd.Str;

for 루프에서처럼 반복자를 암묵적으로 쓰는 대신, 우리는 그것을 $odd-iterator 변수에 명시적으로 할당해서 시퀀스의 홀수 요소만 다뤄요. 그렇게 하면 .skip-one으로 짝수 요소를 건너뛸 수 있어요. 종료를 위해 명시적으로 검사해야 하는데, 그것을 until 표현식에서 해요. 반복할 게 남아 있지 않으면 $oddIterationEnd 값을 가져요. 사용 가능한 메서드와 함수는 Iterator 문서를 참고하세요.

Slip

가끔은 한 리스트의 요소를 다른 리스트 안에 끼워 넣고 싶을 때가 있어요. 이건 Slip이라는 특별한 유형의 리스트로 할 수 있어요.

say (1, (2, 3), 4) eqv (1, 2, 3, 4);         # OUTPUT: «False␤»
say (1, Slip.new(2, 3), 4) eqv (1, 2, 3, 4); # OUTPUT: «True␤»
say (1, slip(2, 3), 4) eqv (1, 2, 3, 4);     # OUTPUT: «True␤»

Slip을 만드는 또 다른 방법은 | 접두사 연산자예요. 이 연산자는 쉼표보다 우선순위가 높아서 단일 값에만 영향을 주지만, 위의 옵션들과 달리 Scalar를 깨뜨린다는 점을 유의하세요.

say (1, |(2, 3), 4) eqv (1, 2, 3, 4);        # OUTPUT: «True␤»
say (1, |$(2, 3), 4) eqv (1, 2, 3, 4);       # OUTPUT: «True␤»
say (1, slip($(2, 3)), 4) eqv (1, 2, 3, 4);  # OUTPUT: «True␤»

느긋한 리스트 (Lazy lists)

List, Seq, ArrayIterator 롤을 구현하는 다른 어떤 클래스든 느긋해질 수 있어요. 값이 요청 시(on demand) 계산되어 나중에 쓰기 위해 저장된다는 뜻이에요. 느긋한 객체를 만드는 방법 중 하나는 gather/take나 시퀀스 연산자를 쓰는 거예요. Iterator 롤을 구현하고 is-lazy 호출에 True를 반환하는 클래스를 직접 쓸 수도 있어요. elems 같은 일부 메서드는 느긋한 List에 호출할 수 없고, 호출하면 Exception이 던져진다는 점을 유의하세요.

# This array is lazy and its elements will not be available
# until explicitly requested.

my @lazy-array = lazy 1, 11, 121 ... 10**100;
say @lazy-array.is-lazy;     # OUTPUT: «True␤»
say @lazy-array[];           # OUTPUT: «[...]␤»

# Once all elements have been retrieved, the list
# is no longer considered lazy.

my @no-longer-lazy = eager @lazy-array;  # Forcing eager evaluation
say @no-longer-lazy.is-lazy;             # OUTPUT: «False␤»
say @no-longer-lazy[];
# OUTPUT: (sequence starting with «[1 11 121» ending with a 300 digit number)

위 예시에서 @lazy-array는 구성 과정을 통해 느긋해진 Array예요. 그것에 is-lazy를 호출하면 실제로는 Iterator 롤이 믹스인한 메서드를 호출하는 건데, 느긋한 리스트에서 비롯됐기 때문에 그 자체도 느긋해요.

느긋한 Seq의 흔한 사용 사례는 무한 수열을 처리하는 것이에요. 값이 아직 계산되지 않았고 전체를 계산할 수도 없는 수열이죠. List의 특정 값은 필요할 때만 계산돼요.

my  $l := 1, 2, 4, 8 ... Inf;
say $l[0..16];
# OUTPUT: «(1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536)␤»

느긋한 객체를 다른 객체에 쉽게 할당하면서 그 느긋함을 보존할 수 있어요.

my  $l := 1, 2, 4, 8 ... Inf; # This is a lazy Seq.
my  @lazy-array = $l;
say @lazy-array[10..15]; # OUTPUT: «(1024 2048 4096 8192 16384 32768)␤»
say @lazy-array.is-lazy; # OUTPUT: «True␤»

불변성 (Immutability)

지금까지 이야기한 리스트들(List, Seq, Slip)은 모두 **불변(immutable)**이에요. 즉 요소를 제거하거나, 기존 요소를 다시 바인딩할 수 없어요.

(1, 2, 3)[0]:delete; # Error Can not remove elements from a List
(1, 2, 3)[0] := 0;   # Error Cannot use bind operator with this left-hand side
(1, 2, 3)[0] = 0;    # Error Cannot modify an immutable Int

하지만 어떤 요소가 Scalar로 감싸져 있다면, 그 Scalar가 가리키는 값을 여전히 바꿀 수 있어요.

my $a = 2;
(1, $a, 3)[1] = 42;
$a.say;            # OUTPUT: «42␤»

즉 불변인 것은 리스트 구조 자체, 그러니까 요소가 몇 개인지와 각 요소의 정체성뿐이에요. 불변성은 요소의 정체성을 넘어 전염되지 않아요.

리스트 문맥 (List contexts)

지금까지는 대부분 중립적인 문맥에서 리스트를 다뤘어요. 사실 리스트는 문법 레벨에서 아주 문맥에 민감해요.

리스트 할당 문맥 (List assignment context)

리스트(또는 리스트로 변환될 무언가)가 @ 시길 변수로의 할당의 오른쪽에 나타나면, 그것은 "적극적으로(eagerly)" 평가돼요. 다시 말해 Seq는 더 이상 만들어낼 요소가 없을 때까지 반복된다는 뜻이에요. 여기는 무한 리스트를 두면 안 되는 곳 중 하나예요. 프로그램이 멈추고 결국 메모리가 고갈되니까요.

my @divisors = (gather {
    for <2 3 5 7> {
        take $_ if 70 %% $_;
    }
});
say @divisors; # OUTPUT: «[2 5 7]␤»

gather 문장은 느긋한 리스트를 만들고, 그것이 @divisors에 할당될 때 적극적으로 평가돼요.

펼치기 "문맥" (Flattening "context")

하위 리스트를 포함하는 리스트가 있고 평평한 단일 리스트만 필요하다면, 리스트를 펼쳐서 모든 괄호를 제거한 것처럼 값의 시퀀스를 만들 수 있어요. 괄호가 아무리 깊이 중첩되어 있어도 동작해요.

say (1, (2, (3, 4)), 5).flat eqv (1, 2, 3, 4, 5) # OUTPUT: «True␤»

이건 문법적인 "문맥"이라기보다는 반복 과정에 더 가깝지만, 문맥처럼 보여요.

리스트 주위의 Scalar는 펼침에 면역이 된다는 점을 유의하세요.

for (1, (2, $(3, 4)), 5).flat { .say } # OUTPUT: «1␤2␤(3 4)␤5␤»

...하지만 @ 시길 변수는 그 요소를 흘려보내요.

my @l := 2, (3, 4);
for (1, @l, 5).flat { .say };      # OUTPUT: «1␤2␤3␤4␤5␤»
my @a = 2, (3, 4);                 # Arrays are special, see below
for (1, @a, 5).flat { .say };      # OUTPUT: «1␤2␤(3 4)␤5␤»

인자 목록(Capture) 문맥 (Argument list (Capture) context)

리스트가 함수나 메서드 호출의 인자로 나타나면 특별한 문법 규칙이 적용돼요. 리스트는 즉시 Capture로 변환돼요. Capture 자체는 List(.list)와 Hash(.hash)를 가져요. 키가 인용되지 않았거나 괄호로 감싸지지 않은 Pair 리터럴은 .list에 들어가지 못해요. 대신 명명 인자(named argument)로 간주되어 .hash로 들어가요. 이 처리의 자세한 내용은 Capture 문서를 참고하세요.

List로부터 새 Array를 만드는 다음 방식들을 생각해볼게요. 이 방식들은 List를 인자 목록 문맥에 두는데, 그 때문에 Array에는 12만 들어가고 :c(3) Pair는 무시돼요.

Array.new(1, 2, :c(3));
Array.new: 1, 2, :c(3);
new Array: 1, 2, :c(3);

대조적으로, 이 방식들은 List를 인자 목록 문맥에 두지 않으므로, :c(3) Pair까지 포함해 모든 요소가 Array에 들어가요.

Array.new((1, 2, :c(3)));
(1, 2, :c(3)).Array;
my @a = 1, 2, :c(3); Array.new(@a);
my @a = 1, 2, :c(3); Array.new: @a;
my @a = 1, 2, :c(3); new Array: @a;

인자 목록 문맥에서 Positional에 적용된 | 접두사 연산자는 항상 리스트 요소를 위치 인자로 Capture에 슬립시켜요. 반면 Associative에 적용된 | 접두사 연산자는 쌍을 명명 매개변수로 슬립시켜요.

my @a := 2, "c" => 3;
Array.new(1, |@a, 4);    # Array contains 1, 2, :c(3), 4
my %a = "c" => 3;
Array.new(1, |%a, 4);    # Array contains 1, 4

슬라이스 인덱싱 문맥 (Slice indexing context)

슬라이스 첨자 내부의 List의 관점에서 보면, 이 문맥은 그저 평범하다는 점에서만 주목할 만해요. 슬라이스에 대한 수식어(adverb)가 ] 뒤에 붙기 때문에, 슬라이스 내부는 인자 목록이 아니고 쌍 형태에 대한 특별한 처리가 일어나지 않아요.

대부분의 Positional 타입은 슬라이스 인덱스의 각 요소에 정수 강제 변환을 적용하므로, 거기 나타나는 쌍은 어쨌든 오류를 만들 거예요.

(1, 2, 3)[1, 2, :c(3)] # OUTPUT: «Method 'Int' not found for invocant of class 'Pair'␤»

...하지만 이건 전적으로 그 타입에 달려 있어요. 타입이 쌍에 대한 순서를 정의한다면 :c(3)을 유효한 인덱스로 간주할 수도 있으니까요.

슬라이스 내부의 인덱스는 보통 자동으로 펼쳐지지 않아요. 그리고 하위 리스트도 보통 Int로 강제 변환되지 않아요. 대신 리스트 구조가 그대로 유지되면서, 결과에서 그 구조를 재현하는 중첩 슬라이스 연산이 발생해요.

say ("a", "b", "c")[(1, 2), (0, 1)] eqv (("b", "c"), ("a", "b")) # OUTPUT: «True␤»

세미콜론으로 구분된 슬라이스 목록인 semilist를 사용하면 여러 차원에 걸쳐 슬라이스도 취할 수 있어요.

my @sliceable = [[ ^10 ], ['a'..'h'], ['Ⅰ'..'Ⅺ']];
say @sliceable[ ^3; 4..6 ]; # OUTPUT: «(4 5 6 e f g Ⅴ Ⅵ Ⅶ)␤»

이것은 처음 세 차원(^3)에서 4번째부터 6번째 요소를 선택하는 거예요.

범위를 슬라이스로 (Range as slice)

Range는 하한과 상한을 담는 컨테이너로, 둘 중 어느 쪽도 제외될 수 있어요. Range로 슬라이스를 만들면 경계 사이의 어떤 인덱스든 포함돼요. 다만 무한 Range는 존재하지 않는 요소를 잘라내요. 상한이 제외된 무한 범위(예: 0..^Inf)는 여전히 무한하며 모든 요소에 도달해요.

my @a = 1..5;
say @a[0..2];     # OUTPUT: «(1 2 3)␤»
say @a[0..^2];    # OUTPUT: «(1 2)␤»
say @a[0..*];     # OUTPUT: «(1 2 3 4 5)␤»
say @a[0..^*];    # OUTPUT: «(1 2 3 4 5)␤»
say @a[0..Inf-1]; # OUTPUT: «(1 2 3 4 5)␤»

상한이 그냥 Whatever가 아니라 WhateverCode면, 범위는 무한이 아니라 Range를 만들어내는 Callable이 돼요. 이건 .. 연산자의 정상적인 동작이에요. 첨자 연산자 []는 리스트의 .elems를 인자로 제공하여 WhateverCode를 평가하고, 그 결과 범위로 슬라이스해요.

say @a[0..*-1];   # OUTPUT: «(1 2 3 4 5)␤»
say @a[0..^*-1];  # OUTPUT: «(1 2 3 4)␤»
# Produces 0..^2.5 as the slice range
say @a[0..^*/2];  # OUTPUT: «(1 2 3)␤»

0..^*0..^*+0는 하나는 무한 범위, 다른 하나는 범위를 만드는 WhateverCode인데도 첨자에서 일관되게 동작한다는 점을 주목하세요. 하지만 0..*+00..*라는 무한 범위와 달리 잘라내지 않기 때문에, 추가로 꼬리에 Nil을 주게 돼요.

배열 생성자 문맥 (Array constructor context)

Array 리터럴 내부에서 초기화 값들의 목록은 capture 문맥이 아니고 평범한 리스트예요. 다만 할당에서처럼 적극적으로 평가돼요.

say so [ 1, 2, :c(3) ] eqv Array.new((1, 2, :c(3))); # OUTPUT: «True␤»
[while $++ < 2 { 42.say; 43 }].map: *.say;           # OUTPUT: «42␤42␤43␤43␤»
(while $++ < 2 { 42.say; 43 }).map: *.say;           # OUTPUT: «42␤43␤42␤43␤»

이제 배열로 넘어가볼게요...

배열 (Arrays)

배열은 리스트와 세 가지 주요한 점에서 달라요. 요소가 타입화될 수 있고, 요소를 자동으로 itemize하며, **가변(mutable)**이에요. 그 외에는 리스트이고, 리스트가 받아들여지는 곳이면 어디든 받아들여져요.

say Array ~~ List     # OUTPUT: «True␤»

네 번째이자 더 미묘한 차이점은, 배열로 작업할 때 느긋함을 유지하거나 무한 시퀀스로 작업하는 게 때로 더 어려울 수 있다는 거예요.

타입화 (Typing)

배열은 타입화될 수 있어서, 그 슬롯(slot)이 할당될 때마다 타입 검사를 수행해요. Int 값만 할당을 허용하는 배열은 Array[Int] 타입이고, Array[Int].new로 만들 수 있어요. @ 시길 변수를 이 목적으로만 쓰려면, 선언할 때 요소 타입을 지정해 그 타입을 바꿀 수 있어요.

my Int @a = 1, 2, 3;              # An Array that contains only Ints
# the same as
my @a of Int = 1, 2, 3;           # An Array of Ints
my @b := Array[Int].new(1, 2, 3); # Same thing, but the variable is not typed
my @b := Array[Int](1, 2, 3);     # Rakudo shortcut for the same code
say @b eqv @a;                    # says True.
my @c = 1, 2, 3;                  # An Array that can contain anything
say @b eqv @c;                    # says False because types do not match
say @c eqv (1, 2, 3);             # says False because one is a List
say @b eq @c;                     # says True, because eq only checks values
say @b eq (1, 2, 3);              # says True, because eq only checks values

@a[0] = 42;                       # fine
@a[0] = "foo";                    # error: Type check failed in assignment

위 예시에서 우리는 타입이 지정되지 않은 @ 시길 변수에 타입화된 Array 객체를 바인딩했어요. 반대 방향은 동작하지 않아요. 잘못된 타입의 Array를 타입화된 @ 시길 변수에 바인딩할 수는 없어요.

my @a := Array[Int].new(1, 2, 3);     # fine
@a := Array[Str].new("a", "b");       # fine, can be re-bound
my Int @b := Array[Int].new(1, 2, 3); # fine
@b := Array.new(1, 2, 3);             # error: Type check failed in binding

타입화된 배열로 작업할 때, 배열이 **명목적 타입(nominally typed)**이라는 점을 기억하는 게 중요해요. 즉 배열의 선언된 타입이 중요하다는 뜻이에요. 다음 서브 선언이 있다고 해볼게요.

sub mean(Int @a) {
    @a.sum / @a.elems
}

Array[Int]를 전달하는 호출은 성공해요.

my Int @b = 1, 3, 5;
say mean(@b);                       # @b is Array[Int]
say mean(Array[Int].new(1, 3, 5));  # Anonymous Array[Int]
say mean(my Int @ = 1, 3, 5);       # Another anonymous Array[Int]

하지만 다음 호출들은 모두 실패해요. 타입화되지 않은 배열을 전달하기 때문이에요. 전달 시점에 그 배열이 우연히 Int 값을 담고 있더라도 말이죠.

my @c = 1, 3, 5;
say mean(@c);                       # Fails, passing untyped Array
say mean([1, 3, 5]);                # Same
say mean(Array.new(1, 3, 5));       # Same again

어떤 컴파일러에서는 배열에 대한 타입 검사를 우회하는 화려한 내부 방식이 있을 수 있다는 점을 유의하세요. 그래서 신뢰할 수 없는 입력을 다룰 때는, 중요한 곳에서 추가 타입 검사를 수행하는 것이 좋은 관행이에요.

for @a -> Int $i { $_++.say };

다만 신뢰할 수 있는 코드 영역에서 평범한 할당 연산만 고수한다면 문제가 되지 않아요. 타입 검사 오류는 컴파일 타임에 잡지 못하면 배열 할당 중에 즉시 발생할 거예요. Raku가 리스트를 다루기 위해 제공하는 핵심 함수 중 그 어떤 것도 깨진 타입화 배열을 만들어내지 않아야 해요.

존재하지 않는 요소(인덱싱할 때)나 Nil이 할당된 요소는 기본값을 취해요. 이 기본값은 is default 트레잇으로 변수마다 조정할 수 있어요. 타입화되지 않은 @ 시길 변수의 요소 타입은 Mu지만, 기본값은 정의되지 않은 Any라는 점을 유의하세요.

my @a;
@a.of.raku.say;                 # OUTPUT: «Mu␤»
@a.default.raku.say;            # OUTPUT: «Any␤»
@a[0].say;                      # OUTPUT: «(Any)␤»
my Numeric @n is default(Real);
@n.of.raku.say;                 # OUTPUT: «Numeric␤»
@n.default.raku.say;            # OUTPUT: «Real␤»
@n[0].say;                      # OUTPUT: «(Real)␤»

고정 크기 배열 (Fixed size arrays)

배열의 차원을 제한하려면 차원이 하나 이상일 때 배열 컨테이너 이름 뒤의 대괄호 안에 차원을 ,;로 구분해 제공하면 돼요. 이런 배열을 **형상 배열(shaped array)**이라고도 불러요. 이런 종류의 Array의 값은 기본적으로 Any예요. 형상은 런타임에 shape 메서드로 접근할 수 있어요.

my @a[2,2];
say @a.raku;
# OUTPUT: «Array.new(:shape(2, 2), [Any, Any], [Any, Any])␤»
say @a.shape;         # OUTPUT: «(2 2)␤»
my @just-three[3] = <alpha beta kappa>;
say @just-three.raku;
# OUTPUT: «Array.new(:shape(3,), ["alpha", "beta", "kappa"])␤»

형상은 차원별로 할당될 수 있는 요소 수를 제어해요.

my @just-two[2] = <alpha beta kappa>;
# Will throw exception: «Index 2 for dimension 1 out of range (must be 0..1)»

고정 크기 Array에 대한 할당은 ListListArrayArray로 승격시켜요 (그 과정에서 가변으로 만들어요).

my @a[2;2] = (1,2; 3,4);
say @a.Array; # OUTPUT: «[1 2 3 4]␤»
@a[1;1] = 42;
say @a.raku;
# OUTPUT: «Array.new(:shape(2, 2), [1, 2], [3, 42])␤»

세 번째 문장이 보여주듯, 형상 배열의 한 요소에도 직접 할당할 수 있어요. 참고: 두 번째 문장은 2018.09 릴리스부터만 동작해요.

6.d 버전부터 열거형도 형상 매개변수로 쓰일 수 있어요.

enum Cards <Trump Ace Deuce Trey>;
my @cards[Deuce;Deuce];
say @cards.shape; # OUTPUT: «(Deuce Deuce)␤»

Itemization

대부분의 용도에서 Array는 각각 올바른 타입의 Scalar를 담고 있는 여러 슬롯으로 구성돼요. 그러한 각 Scalar는 다시 그 타입의 값을 담아요. 배열이 초기화되거나, 할당되거나, 구성될 때 Raku는 값을 자동으로 타입 검사하고 그 값을 담을 Scalar를 만들어요.

이것은 사실 Raku 리스트 처리에서 확실히 이해하기 가장 까다로운 부분 중 하나예요.

먼저, Array에서 itemization이 가정되기 때문에, 직접 넣지 않으면 배열에 할당하는 모든 것 주위에 $(…)이 감싸진다고 생각해야 해요. 반대쪽으로, Array.rakuList.raku와 달리 스칼라를 명시적으로 보여주는 $를 넣지 않아요.

((1, 2), $(3, 4)).raku.say; # says "((1, 2), $(3, 4))"
[(1, 2), $(3, 4)].raku.say; # says "[(1, 2), (3, 4)]"
                            # ...but actually means: "[$(1, 2), $(3, 4)]"

그 많은 달러 기호와 괄호는 사용자에게 이득보다 오히려 눈에 거슬린다고 결정했어요. 기본적으로, 대괄호를 보면 보이지 않는 달러 기호를 기억하세요.

둘째, 그 보이지 않는 달러 기호들이 펼침으로부터도 보호한다는 점을 기억하세요. 그래서 Array 내부의 요소를 평범한 flat이나 .flat 호출로 실제로 펼칠 수는 없어요.

((1, 2), $(3, 4)).flat.raku.say; # OUTPUT: «(1, 2, $(3, 4)).Seq␤»
[(1, 2), $(3, 4)].flat.raku.say; # OUTPUT: «($(1, 2), $(3, 4)).Seq␤»

대괄호 자체는 펼침으로부터 보호하지 않으므로, flat을 써서 Array의 요소를 둘러싼 리스트로 여전히 흘려보낼 수 있어요.

(0, [(1, 2), $(3, 4)], 5).flat.raku.say; # OUTPUT: «(0, $(1, 2), $(3, 4), 5).Seq␤»

...요소 자체는 한 조각으로 남아 있어요.

이것은 당신이 제공한 데이터의 사용자를 괴롭힐 수 있어요. 깊이 중첩된 배열이 있고 그들이 평평한 데이터를 원하는 경우에요. 현재는 중첩을 되돌리려면 구조를 손으로 깊게 매핑해야 해요.

say gather [0, [(1, 2), [3, 4]], $(5, 6)].deepmap: *.take; # OUTPUT: «(0 1 2 3 4 5 6)␤»

...미래의 Raku 버전이 이것을 더 쉽게 만드는 방법을 찾을 수도 있어요. 하지만 itemize되지 않은 리스트로 충분할 때 함수에서 Array나 itemize된 리스트를 반환하지 않는 것은 사용자에 대한 예의로 고려해야 해요.

  • 항상 주변 리스트와 병합하고 싶을 때는 Slip을 사용하세요.
  • 사용자가 펼치기 쉽게 만들고 싶을 때는 itemize되지 않은 리스트를 사용하세요.
  • 사용자가 아마 펼치길 원하지 않을 것을 보호할 때는 itemize된 리스트를 사용하세요.
  • 적절하다면 itemize된 리스트들의 itemize되지 않은 리스트로 Array를 사용하세요. (예: [$(1, 2), $(3, 4)].)
  • 사용자가 먼저 복사하지 않고 결과를 변형하고 싶을 때는 Array를 사용하세요.

배열의 모든 요소가 itemize된다는 사실(스칼라 컨테이너 안에)은 보편적으로 강제되는 규칙이라기보다 **신사 협정(gentleman's agreement)**에 가까워요. 그리고 타입화 배열의 타입 검사보다 덜 강하게 시행돼요. 아래의 "Array 슬롯에 바인딩" 절을 참고하세요.

리터럴 배열 (Literal arrays)

리터럴 Array는 대괄호 안에 List를 두어 구성해요. List는 (가능하면 컴파일 타임에) 적극적으로 반복되고, 그 안의 값은 각각 타입 검사되고 itemize돼요. 대괄호 자체는 펼쳐질 때 요소를 주변 리스트로 흘려보내지만, 요소는 itemization 때문에 흘려보내지지 않아요.

가변성 (Mutability)

리스트와 달리 Array는 가변(mutable)이에요. 요소를 삭제·추가·변경할 수 있어요.

my @a = "a", "b", "c";
@a.say;                  # OUTPUT: «[a b c]␤»
@a.pop.say;              # OUTPUT: «c␤»
@a.say;                  # OUTPUT: «[a b]␤»
@a.push("d");
@a.say;                  # OUTPUT: «[a b d]␤»
@a[1, 3] = "c", "c";
@a.say;                  # OUTPUT: «[a c d c]␤»

할당 (Assigning)

Array에 리스트를 할당하는 것은 적극적(eager)이에요. 리스트는 완전히 평가될 것이므로, 무한해서는 안 돼요. 무한하면 프로그램이 멈출 수 있어요. Array의 슬라이스에 대한 할당도 마찬가지로 적극적이지만, 요청된 요소 수까지만 그래요. 그 수는 유한할 수 있죠.

my @a;
@a[0, 1, 2] = (loop { 42 });
@a.say;                     # OUTPUT: «[42 42 42]␤»

할당 중에 각 값은 그 Array에 허용된 타입인지 타입 검사돼요. 각 값에서 Scalar가 벗겨지고 새로운 Scalar가 그 주위에 감싸져요.

바인딩 (Binding)

개별 Array 슬롯은 $ 시길 변수와 같은 방식으로 바인딩될 수 있어요.

my $b = "foo";
my @a = 1, 2, 3;
@a[2] := $b;
@a.say;          # OUTPUT: «[1 2 "foo"]␤»
$b = "bar";
@a.say;          # OUTPUT: «[1 2 "bar"]␤»

...하지만 Array 슬롯을 값에 직접 바인딩하는 것은 강하게 권장되지 않아요. 그렇게 하면 내장 함수에서 예상치 못한 일을 만날 수 있어요. 이렇게 해야 하는 경우는 값과 Scalar로 감싸진 값의 차이를 아는 가변 컨테이너가 필요하거나, 네이티브 타입 배열을 쓸 수 없는 매우 큰 배열을 다룰 때뿐이에요. 바인딩된 슬롯을 가진 Array는 모르는 사용자에게 절대 제공되어서는 안 돼요.