statement-prefixes — 문장 접두어

statement-prefixes — 문장 접두어 (lazy, eager, hyper, try, do, …)

문장 접두어(statement prefix)는 문장 앞에 붙어서 그 문장의 의미, 출력, 또는 실행되는 시점을 바꿔요. 접두어마다 특정한 동작을 하기 때문에, 어떤 문장이나 문장 묶음에만 적용되는 경우도 있어요. 이 페이지에서는 Raku가 제공하는 문장 접두어들을 하나씩 살펴볼게요.

출처: Statement prefixes - Raku Documentation

lazy

문장 접두어 lazyfor 루프를 포함한 어떤 문장 앞에도 붙을 수 있어요. 실행을 미뤄 두고, 그 결과가 할당된 변수가 실제로 필요해지는 시점에 실행되게 해요.

my $incremented = 0;
my $var = lazy for <1 2 3 4> -> $d {
    $incremented++
};
say $incremented; # OUTPUT: «0␤»
say eager $var;   # OUTPUT: «(0 1 2 3)␤»
say $incremented; # OUTPUT: «4␤»

여기서 $incremented는 lazy 루프를 담은 변수 $var를 eager하게 평가하기 전까지는 증가하지 않아요. 즉 루프의 내부가 실제로 실행되는 건 그때뿐이죠. eager함을 적용하는 방법은 .eager 메서드를 호출하는 등 다양하게 있어요.

my @array = lazy { (^3).map( *² )  };
say @array;       # OUTPUT: «[...]␤»
say @array.eager; # OUTPUT: «[0 1 4]␤»

이 접두어는 gather 앞에도 붙을 수 있는데, 이 경우 안쪽 문장들이 lazy하게 동작해요. 일반적으로 값을 돌려주는 문장 묶음이 있으면 이걸로 lazy하게 만들 수 있어요.

eager

eager 문장 접두어는 뒤따르는 문장들의 결과를 eager하게 돌려줘요. laziness를 버리고 결과를 즉시 얻는 거죠.

my $result := eager gather { for 1..3 { say "Hey"; take $_² } };
say $result[0]; # OUTPUT: «Hey␤Hey␤Hey␤1␤»

gather는 스칼라에 바인딩되면 암시적으로 lazy해져요. 하지만 eager를 문장 접두어로 붙이면 루프의 세 반복이 전부 실행되는데, 그 증거가 출력된 "Hey" 세 번이에요. 우리는 첫 번째 요소만 요청했는데도 말이죠.

hyper

for 루프는 루프에 쓰인 HyperSeqRaceSeq를 자동으로 직렬화해요. 반면 hyperrace는 (아마도 동시적인) 스레드를 써서 루프의 서로 다른 반복을 실행해요:

my @a = hyper for ^100_000 { .is-prime }

이 코드는 맨 for보다 대략 3배 빠르다고 해요. 다만 몇 가지 주의할 점이 있어요.

  • 루프 안의 연산이 스레딩이 의미 있을 만큼 충분히 시간이 걸려야 해요.
  • 루프 안에서 같은 데이터 구조에 읽기/쓰기 접근이 있으면 안 돼요. 루프가 결과를 만들어 내고 그걸 할당하도록 해야 해요.
  • 루프 안에 I/O 연산이 있으면 경합(contention)이 생길 수 있으니 피하는 게 좋아요.

hyperrace의 주요 차이는 결과의 순서예요. 루프 결과가 순서대로 생성되어야 하면 hyper를, 순서가 상관없으면 race를 써요.

quietly

접두어 quietly는 그 뒤의 블록이나 문장이 만들어내는 모든 런타임 경고를 억제해요.

sub marine() {};
quietly say ~&marine; # OUTPUT: «marine␤»
sub told-you { warn 'hey...' };
quietly { told-you; warn 'kaput!' };
warn 'Telling you now!';  # OUTPUT: «Telling you now!␤ [...] ␤»

code.Str을 호출하면 경고가 생겨요. 코드 앞에 quietly를 붙이면 경고 없이 출력만 나오죠.

try

문장 앞에 try를 쓰면 그 문장에서 생긴 예외를 잡아 $! 변수에 저장해요. 블록 앞에서 쓸 때와 똑같은 방식이에요.

try [].pop;
say $!; # OUTPUT: «Cannot pop from an empty Array␤..»

참고로 try 블록에서 Exception이 던져지지 않았다면 $!는 정의되지 않아요.

do

do는 문장 접두어로 쓰여 그 뒤 문장을 명확히 구분해 줘요. 예를 들어 for 문장의 결과를 변수에 할당하고 싶을 때 필요해요. 맨 for는 실패하지만 이렇게는 동작해요:

my $counter = 0;
my $result = do for ^5 { $counter++ };
say $counter; # OUTPUT: «5␤»
say $result;  # OUTPUT: «(0 1 2 3 4)␤»

do는 다른 경우처럼 문장을 괄호로 감싼 것과 동일해요. 좀 더 직관적인 문법으로 그 대안을 쓸 수 있게 해 주는 거죠.

sink

sink 루틴의 경우처럼, sink 접두어는 문장을 실행하고 결과를 버려요. 어떤 문장을 그 부수 효과를 위해 실행하고 싶을 때 사용해요.

my $counter = 0;
my $result = sink for ^5 { $counter++ };
say $counter; #  OUTPUT: «5␤»
say $result;  #  OUTPUT: «(Any)␤»

sink 문장 접두어는 또 Failure를 예외로 변환해요:

sub find-the-number ( Int $n where $n < 10 ) {
    if $n == 7 {
        return True;
    } else {
        fail "Not that number" ;
    }
}
for 1..^10 {
    try {
        sink find-the-number($_);
    };
    say "Found $_" unless $!;
}

이 경우에는 try 블록이 예외를 잡지 못할 때만 그 숫자를 찾았다는 걸 알 수 있어요.

react

react는 동시성 프로그램에서 어떤 이벤트가 발생할 때마다 실행되는 코드 블록을 만들 때 써요. 블록과 함께 동작하고, 문장 접두어로도 동작해요.

my Channel $KXGA .= new;
for ^100 {
    $KXGA.send( (100000..200000).pick );
}

my @sums = ( start react whenever $KXGA -> $number {
    say "In thread ", $*THREAD.id;
    say "→ ", (^$number).sum;
} ) for ^10;

start { sleep 10; $KXGA.close(); }

await @sums;

이 경우 reactwhenever를 접두로 붙여서, 채널에서 읽은 모든 숫자에 대해 긴 합을 계산해요.