Seq 타입

Seq 타입 (raku-type-seq)

값들의 시퀀스, 특히 지연(lazy) 계산될 수 있는 시퀀스를 나타내는 타입이에요. Raku에서 반복 가능한 값들의 흐름을 만날 때마다 Seq를 만나게 돼요.

출처: Raku Docs - Seq

정의

class Seq is Cool does Iterable does Sequence { }

Seq는 값들의 시퀀스를 만들어 낼 수 있는 무엇이든 나타내요. Seq는 태어났을 때는 반복하면 값을 소비하는 상태예요. .cache를 호출하면 생성된 값들을 나중에 접근할 수 있게 저장해요.

Seq를 만드는 고수준 방법으로는 gather/take가 있고, map, grep 같은 내장 메서드들도 많아요. 반복자나 반복문 구조에서 Seq를 만드는 저수준 생성자들도 있어요. 시퀀스 연산자 ...나 그 변형들로도 만들 수 있죠.

my $s = (1...5);
say $s;              # OUTPUT: «(1 2 3 4 5)␤»
say $s.^name;        # OUTPUT: «Seq␤»

Seq의 값을 배열에 할당하면(지연이 아닌 경우) 그 Seq는 소비돼요. 할당 중에 Seq가 반복되는 것을 막으려면 lazy 문 접두사를 쓰세요.

# The Seq created by gather ... take is consumed on the spot here.
my @a = gather do { say 'consuming...'; take 'one' };  # OUTPUT: «consuming...␤»

# The Seq here is only consumed as we iterate over @a later.
my @a = lazy gather do { say 'consuming...'; take 'one' };  # outputs nothing.
.say for @a;  # OUTPUT: «consuming...␤one␤»

IO::Handlelines 메서드가 전형적인 쓰임새예요. 파일에서 읽은 모든 줄을 저장하면 메모리를 많이 쓸 테니까요.

for open('README.md').lines -> $line {
    say $line;
}

이렇게 하면 파일의 모든 줄을 메모리에 들고 있지 않아요. 그런데 여기서 중요한 의미가 따라와요. 같은 Seq 객체를 두 번 반복할 수 없다는 거예요. 그래야 예전 값을 버릴 수 있으니까요. 두 번 돌리면 죽어요.

my @a = 1, 2, 3;
my @b = <a b c>;
my \c = @a Z=> @b;
.say for c;
.say for c; # fails
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Seq::Consumed: This Seq has already been iterated, and its values consumed
# (you might solve this by adding .cache on usages of the Seq, or
# by assigning the Seq into an array)»

주의: 프로그램은 어떤 Seq가 (캐시되지 않았더라도) 오직 한 번만 반복될 거라고 가정하면 안 돼요. 캐싱은 개발자에게 최적화 수단으로 노출된 휘발성 상태예요. .raku(2019.11 릴리스 이전에는 .perl)를 호출하는 것처럼 많은 연산이 Seq를 캐시하게 만들 수 있어요. 6.d 버전부터는 소비된 Seq에도 .raku를 호출할 수 있어요. 만약 프로그램이 Seq를 한 번만 반복할 거라고 가정했는데 나중에 루프 중에 이런 연산을 부르게 바뀐다면 그 가정은 깨져요.

캐시된 Seq에서는 &infix:<eqv>, .Slip, .join, .List, .list, .eager, .Array, .is-lazy가 호출될 때 그 캐시된 리스트를 사용해요.

Seq는 무한하더라도 정규식과 스마트매치할 수 있어요.

my @fib = 1,1, *+* ... *;
say @fib[^1000] ~~ /^9999/; # OUTPUT: «Nil␤»

다만 무한하거나 지연된 Seq는 매칭할 때 활성화(vivify)되면서 무한 루프로 이어질 수 있으니, 검색 범위를 어떻게든 제한해야 해요.

메서드

method new

proto method new(Seq: |) {*}
multi method new(Seq: Iterator:D $iter)
multi method new(Seq:)

단일 인자로 받은 반복자에서 새 Seq 객체를 만들어요. 인자 없이 호출하면 빈 Seq를 만들어요.

method iterator

method iterator(Seq:D:)

Seq가 캐시되지 않았다면 기본 반복자를 돌려주고 invocant를 소비된 것으로 표시해요. 이미 소비된 시퀀스에서 호출하면 X::Seq::Consumed 타입 오류를 던져요. 그 외에는 캐시된 리스트에 대한 반복자를 돌려줘요.

method is-lazy

method is-lazy(Seq:D:)

기본 반복자나 캐시된 리스트가 스스로 지연(lazy)이라고 여길 때만 True를 돌려줘요. 이미 소비된 시퀀스에서 호출하면 X::Seq::Consumed 오류를 던져요.

method Seq

multi method Seq(Seq:D:)

객체를 복제(clone)해요.

method Capture

method Capture()

객체를 List로 강제 변환하고, 다시 그 ListCapture로 강제 변환해요.

method elems

method elems(Seq:D:)

시퀀스의 값 개수를 돌려줘요. 개수를 미리 알 수 없으면 Seq를 캐시하고 끝까지 평가해요. 무한 시퀀스는 끝까지 평가할 수 없으니 지연으로 선언해야 해요. 지연된 Seq.elems를 호출하면 X::Cannot::Lazy로 실패해요.

method from-loop

multi method from-loop(&body, :$label)
multi method from-loop(&body, &cond, :$repeat!, :$label)
multi method from-loop(&body, &cond, :$label)
multi method from-loop(&body, &cond, &afterwards, :$label)

이 메서드들은 새 Seq 기반 콜백을 만들어요. 일반적으로 새 요소가 요청될 때마다 &body를 호출하고 그 반환값을 항목으로 써서 무한 Seq를 만들어요. 이는 loop { body } 구문을 흉내내는(구현하는) 거예요.

&cond가 포함된 multi는 &body 호출 전마다 그 조건을 평가해요. &cond가 거짓을 돌려주면 시퀀스를 끝내요. $repeat가 참이면 첫 &cond 호출을 생략하고 바로 &body를 불러요. 이는 while cond { body }repeat { body } while cond 루프를 흉내내요. &afterward가 있으면 매 &body 호출 후에 불려요.

method sink

method sink(--> Nil)

Iterator라면 sink-all을, Sequence가 리스트라면 sink를 호출해요.

say (1 ... 1000).sink; # OUTPUT: «Nil␤»

값을 만들어 내는 과정의 부수 효과를 얻고 싶을 때 쓸 수 있어요.

method skip

multi method skip(Seq:D:)
multi method skip(Seq:D: Whatever)
multi method skip(Seq:D: Callable:D $w)
multi method skip(Seq:D: Int() $n)
multi method skip(Seq:D: $skip, $produce)

다음 값 중 $n개를 버린 뒤 남은 것을 담은 Seq를 돌려줘요. $n이 음수면 0으로 친다. WhateverCode를 넘겨 끝에서 몇 개를 버릴지 지정할 수도 있어요. 지연된 Seq에서는 요청한 만큼 버려질 때까지 블록돼요.

say (1..5).Seq.skip;      # OUTPUT: «(2 3 4 5)␤»
say (1..5).Seq.skip(3);   # OUTPUT: «(4 5)␤»
say (1..5).Seq.skip(5);   # OUTPUT: «()␤»
say (1..5).Seq.skip(-1);  # OUTPUT: «(1 2 3 4 5)␤»

Whatever로 호출하면 빈 Seq를 돌려줘요.

say <1 2 3>.Seq.skip(*);  # OUTPUT: «()␤»

Callable을 쓰는 multi는 주로 이렇게 사용해요.

say (1..5).Seq.skip(*-3); # OUTPUT: «(3 4 5)␤»

이 경우 처음 $n개를 버리는 대신 WhateverCode가 가리키는 요소(여기서는 마지막 세 개)를 제외한 나머지를 다 버려요.

언어 버전 6.e부터(라쿠도 컴파일러 2022.12+에 초기 구현 존재) 여러 인자를 지정할 수도 있어요. 이들은 "몇 개 만들고, 몇 개 버리고, 또 몇 개 만들고..."로 해석돼요.

say (1..12).Seq.skip(2,3,4); # OUTPUT: «(3 4 5 10 11 12)␤»

여기선 2개를 버리고(1,2), 3개를 만들고(3,4,5), 4개를 버리고(6..9), 나머지(10,11,12)를 만들어요. 마지막 값이 "만들기" 자리에 있으면 나머지 Seq는 버려지고, "버리기" 자리에 있으면 나머지 Seq는 만들어진다는 점에 주의하세요.

say (1..10).Seq.skip(2,3,1,2); # OUTPUT: «(3 4 5 7 8)␤»
say (1..10).Seq.skip(2,3,1);   # OUTPUT: «(3 4 5 7 8 9 10)␤»

"만들기" 자리에 Whatever가 있으면 나머지를 만들고, 그 외에는 나머지를 버려요.

say (1..10).Seq.skip(2,*);   # OUTPUT: «(3 4 5 6 7 8 9 10)␤»
say (1..10).Seq.skip(2,3,*); # OUTPUT: «(3 4 5)␤»

버리는 대신 만들기부터 시작하고 싶다면 첫 값을 0으로 지정하세요.

say (1..10).Seq.skip(0,3,4); # OUTPUT: «(1 2 3 8 9 10)␤»

끝없는 버리고-만들기 패턴을 반복하고 싶다면 인자들 자체를 무한 Seq로 줄 수도 있어요.

say (^20).Seq.skip(|(2,3) xx *); # OUTPUT: «(0 1 5 6 10 11 15 16)␤»

multi method slice

method slice(Seq:D: *@indices --> Seq:D)

라쿠도 컴파일러 2021.02 릴리스부터 사용할 수 있어요. slice 메서드는 단조 증가하는 인덱스들을 받아 그 위치의 값들을 새 Seq로 만들어요. 인덱스는 숫자 하나일 수도 범위일 수도 있는데, 값이 증가하는 방향이어야 해요.

이 방법은 Seq를 캐시하거나 List/Array로 바꾸는 것보다 특정 값들을 빼내는 더 효율적인 방법이에요.

say (1..10).Seq.slice(0, 3..6, 8);  # OUTPUT: «(1 4 5 6 7 9)␤»