제어 흐름
제어 흐름 (control)
프로그램의 흐름을 제어하는 방법은 모든 언어의 핵심이에요. Raku의 제어 흐름 구문은 자매품인 Perl의 그것과 겉보기에 비슷하면서도, 조건·블록·반복이 값을 만들어내는 방식이나 주제 변수($_)의 취급에서 상당히 다른 면이 있어요. 이 문서는 Raku의 문(statement)과 블록부터 if·unless·with 같은 조건문, for·while·loop 같은 반복문, 그리고 given·when의 토픽화까지를 정리한, 제어 흐름의 종합 안내서예요.
본문
Statements
Raku 프로그램은 하나 이상의 문(statement)으로 구성돼요. 단순 문들은 세미콜론으로 구분돼요. 다음 프로그램은 "Hello"를 출력하고 다음 줄에 "World"를 출력해요.
say "Hello";
say "World";
문에서 공백이 나타나는 대부분의 자리와 세미콜론 앞에서는 여러 줄로 나눌 수 있어요. 또한 여러 문이 같은 줄에 올 수도 있어요. 조금 이상해 보이겠지만, 위 예는 이렇게도 쓸 수 있어요.
say
"Hello"; say "World";
Blocks
다른 많은 언어처럼 Raku는 {와 }로 둘러싸인 blocks를 사용해 일련의 문을 단일 Block으로 만든다. 블록의 마지막 문과 닫는 } 사이의 세미콜론은 생략해도 괜찮아요.
{ say "Hello"; say "World" }
블록이 문으로 단독으로 서 있으면, 이전 문이 끝나자마자 그 블록이 실행되고 그 안의 문들이 실행돼요.
say 1; # OUTPUT: «1»
{ say 2; say 3 }; # OUTPUT: «23»
say 4; # OUTPUT: «4»
문으로 단독으로 서 있지 않는 한, 블록은 그저 클로저를 만들어요. 안의 문들은 즉시 실행되지 않아요. 클로저는 또 다른 주제이고, 사용법은 다른 곳에서 설명돼요. 지금은 블록이 언제 실행되고 언제 실행되지 않는지 이해하는 게 중요해요.
say "We get here";
{ say "then here." };
{ say "not here"; 0; } or die;
위 예에서 첫 문을 실행한 뒤, 첫 번째 블록이 두 번째 문으로 단독으로 서 있으므로 그 안의 문을 실행해요. 두 번째 블록은 클로저라서, 대신 Block 타입의 객체를 만들 뿐 실행하지 않아요. 객체 인스턴스는 보통 참으로 간주되므로, 그 블록이 실행된다면 0으로 평가될지라도 코드는 죽지 않아요. 이 예는 그 Block 객체로 뭘 할지 말하지 않으므로 그냥 버려져요.
아래에서 다루는 대부분의 흐름 제어 구문은 Raku에게 두 번째 블록 같은 것을 언제, 어떻게, 몇 번이나 진입할지를 알려주는 방법일 뿐이에요.
그것들로 들어가기 전에, 문법에 대한 중요한 부담을 하나 짚을게요. 세미콜론을 넣을 만한 자리인 닫는 중괄호 } 뒤 줄에 아무것도(또는 주석만) 없다면 세미콜론이 필요 없어요.
# All three of these lines can appear as a group, as is, in a program
{ 42.say } # OUTPUT: «42»
{ 43.say } # OUTPUT: «43»
{ 42.say }; { 43.say } # OUTPUT: «4243»
...하지만:
{ 42.say } { 43.say } # Syntax error
{ 42.say; } { 43.say } # Also a syntax error, of course
그러니 줄바꿈하는 편집기에서 백스페이스를 칠 때 조심해야 해요.
{ "Without semicolons line-wrapping can be a bit treacherous.".say } \
{ 43.say } # Syntax error
대부분의 언어에서도 실수로 주석 처리되는 걸 막으려고 이 부분에 주의해야 해요. 아래 예제들 중 상당수는 명확성을 위해 불필요한 세미콜론이 있을 수 있어요.
클래스 본문은 최상위 표현식에 대해 단순 블록처럼 동작해요. role이나 grammer(사실 클래스임)·module 같은 다른 패키지도 마찬가지예요.
class C {
say "I live";
die "I will never live!"
};
my $c = C.new;
# OUTPUT: Fails and writes «I liveI will never live!
이 블록은 먼저 첫 문을 실행한 다음 die하며 두 번째 문을 출력해요. $c는 결코 값을 얻지 못해요.
Phasers
블록에는 phaser가 있을 수 있어요. 실행을 특정 단계로 나누는 특별한 표시가 있는 블록이에요. 자세한 내용은 phasers 페이지를 보세요.
do
단독 문으로 설 수 없는 곳에서 블록을 실행하는 가장 간단한 방법은 그 앞에 do를 쓰는 거예요.
# This dies half of the time
do { say "Heads I win, tails I die."; Bool.pick } or die; say "I win.";
do와 블록 사이에 공백이 있어야 한다는 점에 주의하세요.
do {...} 전체는 블록의 최종 값으로 평가돼요. 그 값이 표현식의 나머지를 평가하는 데 필요해질 때 블록이 실행돼요. 그래서:
False and do { 42.say };
...는 42를 출력하지 않아요. 그러나 블록은 그것을 포함하는 표현식이 평가될 때마다 한 번씩만 평가돼요.
# This says "(..1 ..2 ..3)" not "(..1 ...2 ....3)"
my $f = "."; say do { $f ~= "." } X~ 1, 2, 3;
즉, 다른 모든 것과 같은 reification 규칙을 따르는 거예요.
기술적으로 do는 정확히 한 번의 반복만 실행하는 루프예요.
do는 중괄호 없는 bare 문에도 쓸 수 있지만, 이는 주로 표현식의 마지막인 문을 괄호로 감싸야 하는 문법적 필요를 피하는 데 유용해요.
3, do if 1 { 2 } ; # OUTPUT: «(3, 2)»
3, (if 1 { 2 }) ; # OUTPUT: «(3, 2)»
3, if 1 { 2 } ; # Syntax error
결과적으로 do는 문법상 함수여야 하는 블록은 실행하지 않아요. 예를 들어 ->로 시그니처를 지정하면 do는 이를 단일 표현식 문으로 취급해요.
따라서 첫 예에 ->를 추가하면 클로저가 평가되지 않아요.
# This never dies and never prints "Heads I win, tails I die."
do -> { say "Heads I win, tails I die."; Bool.pick } or die; say "I win.";
start
문이나 블록을 비동기로 실행하는 가장 간단한 방법은 그 앞에 start를 쓰는 거예요.
start { sleep 1; say "done" }
say "working";
# working, done
start와 블록 사이에 공백이 있어야 한다는 점에 주의하세요. 위 예에서 start 블록은 변수에 할당되지 않으므로 sink 컨텍스트에 있어요. 6.d 버전부터 이렇게 sink된 블록에는 예외 핸들러가 붙어요.
start { die "We're dead"; }
say "working";
sleep 10;
이 코드는 6.d 버전에서는 Unhandled exception in code scheduled on thread 4 We're dead를 출력하고, 6.c 버전에서는 10초를 기다린 후 그냥 빠져나와요.
start {...}는 즉시 Promise를 반환하는데, 블록의 결과에 관심이 없다면 안전하게 무시할 수 있어요. 블록의 최종 값에 관심이 있다면 반환된 promise에서 .result 메서드를 호출하면 돼요. 그래서:
my $promise = start { sleep 10; 42 }
# ... do other stuff
say "The result is $promise.result()";
블록 안의 코드가 아직 끝나지 않았다면, .result 호출은 끝날 때까지 기다려요.
bare 문에 사용된 start는 비동기로 할 일이 서브루틴·메서드 하나뿐일 때 유용해요.
sub get42 { 42 }
my $promise = start get42;
say $promise.result; # OUTPUT: «42»
start 코드는 바깥 블록의 특수 변수 $!와 $/에 접근하지 못하고 새 것을 받는다는 점에 주의하세요. 그래서 모든 비동기 작업은 작업별 상태를 가져요.
따라서 비동기 작업에서 실행되는 try 표현식과 정규식 매치는 작업별 상태를 가져요.
'a' ~~ /a/; # $/ is set to 「a」
try die; # $! is defined now with an anonymous AdHoc exception
# as a code block
await start { say $! }; # OUTPUT: «Nil»
await start { say $/ }; # OUTPUT: «Nil»
# as a single statement
await start $!.say; # OUTPUT: «Nil»
await start $/.say; # OUTPUT: «Nil»
if
조건부로 코드 블록을 실행하려면 if 다음에 조건을 쓰세요. 조건(표현식)은 if 앞의 문이 끝난 직후 평가돼요. 조건에 붙은 블록은 그 조건이 Bool로 강제 변환되었을 때 True를 뜻할 때만 평가돼요. 다른 언어와 달리 조건에 괄호를 칠 필요 없고, 대신 블록 주위의 {와 }는 필수예요.
if 1 { "1 is true".say } ; # says "1 is true"
if 1 "1 is true".say ; # syntax error, missing block
if 0 { "0 is true".say } ; # does not say anything, because 0 is false
if 42.say and 0 { 43.say }; # says "42" but does not say "43"
"문장 수식어(statement modifier)" 형태의 if도 있어요. 이 경우 if와 조건이 조건부로 실행하고 싶은 코드 뒤에 와요. 그래도 조건은 여전히 항상 먼저 평가된다는 점에 주의하세요.
43.say if 42.say and 0; # says "42" but does not say "43"
43.say if 42.say and 1; # says "42" and then says "43"
say "It is easier to read code when 'if's are kept on left of screen"
if True; # says the above, because it is true
{ 43.say } if True; # says "43" as well
문장 수식어 형태는 아껴 쓰는 게 아마 가장 좋아요.
if 문 자체는 블록을 실행하지 않으면 빈 목록을 Slip하거나, 블록이 만들어내는 값을 반환해요.
my $d = 0; say (1, (if 0 { $d += 42; 2; }), 3, $d); # says "(1 3 0)"
my $c = 0; say (1, (if 1 { $c += 42; 2; }), 3, $c); # says "(1 2 3 42)"
say (1, (if 1 { 2, 2 }), 3); # does not slip, says "(1 (2 2) 3)"
문장 수식어도 같지만, 블록 대신 문의 값을 갖는다는 차이가 있어요.
say (1, (42 if True) , 2); # says "(1 42 2)"
say (1, (42 if False), 2); # says "(1 2)"
say (1, 42 if False , 2); # says "(1 42)" because "if False, 2" is true
if는 기본적으로 주제($_)를 바꾸지 않아요. 조건부 표현식이 만들어낸 값에 접근하려면 더 강하게 요청해야 해요.
$_ = 1; if 42 { $_.say } ; # says "1"
$_ = 1; if 42 -> $_ { $_.say } ; # says "42"
$_ = 1; if 42 -> $a { $_.say; $a.say } ; # says "1" then says "42"
$_ = 1; if 42 { $_.say; $^a.say } ; # says "1" then says "42"
이것은 "무언가가 False로 평가되지 않으면, 그것으로 무언가 하라" 같은 패턴에 특히 유용해요.
sub get-user-id($username) { #`(return 0 if no such username exists) }
if prompt 'Enter username: ' -> $username {
if get-user-id($username) -> $user-id {
say "Found id $user-id for name $username";
}
}
여기서 $username과 $user-id는 각자의 if 블록 안에서만 정의돼요. 그 if 블록은 그들이 True임을 보장해요. 그래서 이 해결책은 더 관례적인 my $username = …; if $username {…}보다 장점이 있어요.
정의됨(definedness)을 진리값 대신 검사하고 기본적으로 토픽화하는 with와 비교해 보세요. (위 예의 두 번째 if는, get-user-id 루틴이 존재하지 않는 사용자명에 0 대신 Nil을 반환한다면 with로 대체할 수 있어요.)
else/elsif
복합 조건문은 if 조건 다음에 else를 붙여 조건식이 거짓일 때 실행할 대체 블록을 만들어 낼 수 있어요.
if 0 { say "no" } else { say "yes" } ; # says "yes"
if 0 { say "no" } else{ say "yes" } ; # says "yes", space is not required
else는 조건문과 세미콜론으로 분리할 수 없어요. 하지만 특별한 경우로 줄바꿈은 괜찮아요.
if 0 { say "no" }; else { say "yes" } ; # syntax error
if 0 { say "no" }
else { say "yes" } ; # says "yes"
if와 else 사이에 elsif로 추가 조건을 끼울 수 있어요. 추가 조건은 그 앞의 모든 조건이 거짓일 때만 평가되고, 참인 첫 조건 옆의 블록만 실행돼요. 원한다면 else 대신 elsif로 끝낼 수도 있어요.
if 0 { say "no" } elsif False { say "NO" } else { say "yes" } # says "yes"
if 0 { say "no" } elsif True { say "YES" } else { say "yes" } # says "YES"
if 0 { say "no" } elsif False { say "NO" } # does not say anything
sub right { "Right!".say; True }
sub wrong { "Wrong!".say; False }
if wrong() { say "no" } elsif right() { say "yes" } else { say "maybe" }
# The above says "Wrong!" then says "Right!" then says "yes"
else나 elsif에는 문장 수식어 형태를 쓸 수 없어요.
42.say if 0 else { 43.say } # syntax error
세미콜론과 줄바꿈에 관한 모든 규칙도 일관되게 적용돼요.
if 0 { say 0 }; elsif 1 { say 1 } else { say "how?" } ; # syntax error
if 0 { say 0 } elsif 1 { say 1 }; else { say "how?" } ; # syntax error
if 0 { say 0 } elsif 1 { say 1 } else { say "how?" } ; # says "1"
if 0 { say 0 } elsif 1 { say 1 }
else { say "how?" } ; # says "1"
if 0 { say 0 }
elsif 1 { say 1 } else { say "how?" } ; # says "1"
if 0 { say "no" }
elsif False { say "NO" }
else { say "yes" } ; # says "yes"
전체가 (블록이 실행되지 않았다면) 빈 목록을 Slip하거나, 실행된 블록이 만들어낸 값을 반환해요.
my $d = 0; say (1,
(if 0 { $d += 42; "two"; } elsif False { $d += 43; 2; }),
3, $d); # says "(1 3 0)"
my $c = 0; say (1,
(if 0 { $c += 42; "two"; } else { $c += 43; 2; }),
3, $c); # says "(1 2 3 43)"
else 안에서 이전 표현식(if 또는 있다면 마지막 elsif에서 온)의 값을 얻을 수 있어요.
$_ = 1; if 0 { } else -> $a { "$_ $a".say } ; # says "1 0"
$_ = 1; if False { } else -> $a { "$_ $a".say } ; # says "1 False"
if False { } elsif 0 { } else -> $a { $a.say } ; # says "0"
unless
"if not (X)"라고 계속 치는 게 지겨울 때 unless를 써서 조건문의 의미를 뒤집을 수 있어요. unless에는 else나 elsif를 쓸 수 없어요. 헷갈리기 때문이에요. 이 두 차이를 제외하면 unless는 if와 똑같이 동작해요.
unless 1 { "1 is false".say } ; # does not say anything, since 1 is true
unless 1 "1 is false".say ; # syntax error, missing block
unless 0 { "0 is false".say } ; # says "0 is false"
unless 42.say and 1 { 43.say } ; # says "42" but does not say "43"
43.say unless 42.say and 0; # says "42" and then says "43"
43.say unless 42.say and 1; # says "42" but does not say "43"
$_ = 1; unless 0 { $_.say } ; # says "1"
$_ = 1; unless 0 -> $_ { $_.say } ; # says "0"
$_ = 1; unless False -> $a { $a.say } ; # says "False"
my $c = 0; say (1, (unless 0 { $c += 42; 2; }), 3, $c); # says "(1 2 3 42)"
my $d = 0; say (1, (unless 1 { $d += 42; 2; }), 3, $d); # says "(1 3 0)"
with orwith without
with 문은 if와 비슷하지만 진리값이 아니라 정의됨(definedness)을 검사하고, given처럼 조건을 토픽화해요.
with "abc".index("a") { .say } # prints 0
elsif와 비슷하게 orwith로 정의됨 검사를 연결할 수 있어요.
# The below code says "Found 'a' at 0"
my $s = "abc";
with $s.index("a") { say "Found 'a' at $_" }
orwith $s.index("b") { say "Found 'b' at $_" }
orwith $s.index("c") { say "Found 'c' at $_" }
else { say "Didn't find 'a', 'b' or 'c'" }
if 기반 절과 with 기반 절을 섞을 수도 있어요.
# This says "Yes"
if 0 { say "No" } orwith Nil { say "No" } orwith 0 { say "Yes" };
unless처럼 without으로 미정의(undefinedness)를 검사할 수 있지만, else 절은 추가할 수 없어요.
my $answer = Any;
without $answer { warn "Got: {$_.raku}" }
또한 with와 without 문장 수식어도 있어요.
my $answer = (Any, True).roll;
say 42 with $answer;
warn "undefined answer" without $answer;
다른 연결 가능한 구문처럼, with/if..orwith/elsif 체인을 완성하는 else는 자신이 이전 (실패한) 조건의 토픽(with의 토픽 또는 마지막 orwith·elsif의 토픽)을 토픽화해요.
with나 orwith 뒤에 오는 else의 경우, 미정의가 보장된 값을 토픽화하는 게 쓸모없어 보일 수 있어요. 하지만 실패할 수 있는 연산과 함께 쓰면 유용한 관용구가 돼요. Failure 값은 항상 미정의이니까요.
sub may_fail( --> Numeric:D ) {
my $value = (^10).pick || fail "Zero is unacceptable";
fail "Odd is also not okay" if $value % 2;
return $value;
}
with may_fail() -> $value { # defined, so didn't fail
say "I know $value isn't zero or odd."
} else { # undefined, so failed, and the Failure is the topic
say "Uh-oh: {.exception.message}."
}
Failure를 토픽화하면 그것이 handled로 표시되므로 — with/else로 안전하게 실행을 계속할 수 있어요 — 하지만 Failure 값 자체를 안전하게 만들지는 않아요. else 절 안에서도 값을 직접 사용하려 하면 그 else 절 자체가 실패하거나(Rakudo에서는 Failure를 던져진 예외로 "승격"시킴) 해요.
하지만 위에서 봤듯이, else가 토픽화하는 handled Failure 객체의 메서드 — exception 같은 것 — 은 사용할 수 있어요. 진단을 제공하거나 밑의 Exception을 조사하고 싶다면요.
when
when 블록은 if 블록과 비슷하고 둘 다 바깥 블록에서 쓸 수 있어요. 둘 다 "문장 수식어" 형태도 있어요. 하지만 같은 바깥 블록에서 뒤따르는 코드의 처리 방식에 차이가 있어요. when 블록이 실행되면 제어가 바깥 블록으로 넘어가고 뒤따르는 문들은 무시돼요. 하지만 if 블록이 실행되면 뒤따르는 문들이 실행돼요. 다음 예들은 if·when 블록에 특별한 탈출이나 다른 부수 효과 문이 포함되어 있지 않다고 가정할 때의 기본 동작을 보여줘요.
{
if X {...} # if X is true in Boolean context, block is executed
# following statements are executed regardless
}
{
when X {...} # if X is true in Boolean context, block is executed
# and control passes to the outer block
# following statements are NOT executed
}
위 if·when 블록이 파일 스코프에 나타나면, 각 경우에 뒤따르는 문들이 실행돼요.
when에는 if에 없는 기능이 하나 더 있어요. when의 Boolean 컨텍스트 검사는 기본적으로 $_ ~~인 반면 if의 것은 아니에요. 이는 $_ 값 없이 when 블록에서 X를 어떻게 쓰는지에 영향을 줘요. (그 경우 $_는 Any이고 Any는 True에 스마트매치돼요: Any ~~ True는 True를 산출해요.) 다음을 생각해 보세요.
{
my $a = 1;
my $b = True;
when $a { say 'a' }; # no output
when so $a { say 'a' } # a ("so $a" 'so' coerces $a to Boolean context True
# which matches with Any)
when $b { say 'b' }; # no output (this statement won't be run)
}
마지막으로 when의 문장 수식어 형태는 다른 블록 안이나 밖에서 뒤따르는 문의 실행에 영향을 주지 않아요.
say "foo" when X; # if X is true statement is executed
# following statements are not affected
성공한 매치는 블록을 빠져나가므로, 다음 코드 조각의 동작은:
$_ = True;
my $a;
{
$a = do when .so { "foo" }
};
say $a; # OUTPUT: «(Any)»
어떤 값이 저장되거나 처리되기 전에 do 블록이 버려지기 때문으로 설명돼요. 하지만 이 경우:
$_ = False;
my $a;
{
$a = do when .so { "foo" }
};
say $a; # OUTPUT: «False»
비교가 거짓이라 블록이 버려지지 않으므로, $a는 실제로 값을 얻게 돼요.
for
for 루프는 목록을 반복하며, 각 반복에서 Block 안의 문을 한 번 실행해요. 블록이 파라미터를 받으면 목록의 요소들이 인자로 제공돼요. 기본적으로 블록은 $_ 하나의 파라미터를 받아요.
my @foo = 1..3;
for @foo { $_.print } # prints each value contained in @foo
for @foo { .print } # same thing, because .print implies a $_ argument
for @foo { 42.print } # prints 42 as many times as @foo has elements
포인티 블록(pointy block) 문법이나 placeholder로 파라미터에 이름을 붙일 수 있어요.
my @foo = 1..3;
for @foo -> $item { print $item }
for @foo { print $^item } # same thing
여러 파라미터를 선언할 수 있고, 그 경우 반복자는 블록을 실행하기 전에 목록에서 필요한 만큼 많은 요소를 가져와요.
my @foo = 1..3;
for @foo.kv -> $idx, $val { say "$idx: $val" }
my %hash = <a b c> Z=> 1,2,3;
for %hash.kv -> $key, $val { say "$key => $val" }
for 1, 1.1, 2, 2.1 { say "$^x < $^y" } # OUTPUT: «1 < 1.12 < 2.1»
포인티 블록의 파라미터는 기본값을 가질 수 있어, 누락된 요소가 있는 목록을 처리할 수 있게 해 줘요.
my @list = 1,2,3,4;
for @list -> $a, $b = 'N/A', $c = 'N/A' {
say "$a $b $c"
}
# OUTPUT: «1 2 34 N/A N/A»
for 루프의 블록에 파라미터가 지정되지 않으면, 그 안에서 when을 given 블록에서처럼 비슷하게 사용할 수 있어요.
# A solution for FizzBuzz:
for 1..100 {
when * %% 15 { say 'FizzBuzz' }
when * %% 3 { say 'Fizz' }
when * %% 5 { say 'Buzz' }
default { say $_ }
}
for의 후치(postfix) 형태를 쓰면 블록이 필요 없고 주제가 문 목록에 설정돼요.
say „I $_ butterflies!“ for <♥ ♥ ♥>;
# OUTPUT: «I ♥ butterflies!I ♥ butterflies!I ♥ butterflies!»
for는 지연 목록에도 쓸 수 있어요. 필요할 때만 목록에서 요소를 가져오므로, 파일을 한 줄씩 읽으려면 이것을 쓸 수 있어요.
for $*IN.lines -> $ln { $ln.say }
반복 변수는 항상 렉시컬이라 적절한 스코프를 주기 위해 my를 쓸 필요가 없어요. 또한 읽기 전용 별명이에요. 쓰기 가능하게 하려면 -> 대신 <->를 쓰세요. 또는 is rw 트레이트를 추가할 수도 있어요. 이 트레이트는 바인딩 연산을 수행해 파라미터에 할당하면 호출 측의 변수 값을 바꿔요. 블록 안에서 인자의 복사본을 수정하고 싶다면 is copy를 추가하세요.
my @foo = 1..3;
for @foo <-> $value {
$value = $value %% 2 ?? "Even" !! "Odd"
}
say @foo; # OUTPUT: «[Odd Even Odd]»
@foo = 1..3;
for @foo -> $value is rw {
$value = $value %% 2 ?? "Even" !! "Odd"
}
say @foo; # OUTPUT: «[Odd Even Odd]»
@foo = 1..3;
my @bar;
for @foo -> $value is copy {
$value = $value %% 2 ?? "Even" !! "Odd";
@bar.push: $value
}
say @foo; # OUTPUT: «[1 2 3]»
say @bar; # OUTPUT: «[Odd Even Odd]»
이 규칙은 주제 변수 $_에도 적용돼요. 기본적으로 읽기-쓰기 별명인데, -> 루프에서 쓰이면 읽기 전용이 돼요.
my @foo = 1..3;
for @foo -> $_ { $_.say }
# Error: ...require mutable arguments
for @foo -> $_ { $_++ }
for 루프는 붙은 블록의 각 실행이 만들어내는 값들의 List을 만들어낼 수 있어요. 이 값을 잡으려면 for 루프를 괄호에 넣거나 배열에 할당하세요.
(for 1, 2, 3 { $_ * 2 }).say; # OUTPUT: «(2 4 6)»
my @a = do for 1, 2, 3 { $_ * 2 }; @a.say; # OUTPUT: «[2 4 6]»
my @b = (for 1, 2, 3 { $_ * 2 }); @b.say; # OUTPUT: «[2 4 6]»
이것은 루프 결과가 할당되지 않으면 sink 컨텍스트에 있다는 걸 의미해요.
class Sunk {
has $.titanic;
method sink {
say "Sinking $!titanic";
}
}
Sunk.new( :titanic($_) ) for ^3;
for 1 {
say "About to sink";
Sunk.new( :titanic($_) );
}
# OUTPUT:
# Sinking 0
# Sinking 1
# Sinking 2
# About to sink
# Sinking 1
첫 루프는 세 요소를 만들지만 그것들이 sink 컨텍스트에 있으므로 sink 메서드가 호출돼요. 두 번째 루프에서는 마지막 문이 sink 컨텍스트에 있으므로 그것도 sink돼요 (6.d 버전부터).
Empty 상수는 루프에 대해 no-op으로 동작해요.
say "Not here" for Empty;
아무것도 하지 않아요. 이 상수는 빈 Slip나 List와 동등해요.
미정의 값도 같은 방식으로 동작해요.
my @array := Empty;
.say for @array;
say @array; # OUTPUT: «()»
Empty를 할당하면 Array를 사실상 미정의로 만들고, 미정의 배열에서 for를 쓰면 루프에 진입조차 하지 않아요. 보여준 것처럼 Empty를 직접 쓴 위 예와 사실상 같은 동작을 해요.
hyper와 race로 for 루프를 병렬로 반복할 수 있어요. 자세한 내용은 Map 클래스의 hyper와 race 문서도 보세요.
my $primes_h = hyper for ^10_000 -> $number { $number if $number.is-prime };
say $primes_h.elems; # OUTPUT: «1229»
say $primes_h.tail: 5; # OUTPUT: «(9931 9941 9949 9967 9973)»
hyper의 경우 요소 순서가 보존돼요.
my $primes_r = race for ^10_000 -> $number { $number if $number.is-prime };
say $primes_r.elems; # OUTPUT: «1229»
hyper와 달리 race는 요소 순서를 보존하지 않아요.
gather/take
gather는 sequence 값을 반환하는 문·블록 접두사예요. 값들은 gather 코드의 동적 스코프에서 take 호출에서 나와요. 다음 예에서 정수의 인수를 gather로 계산하는 서브루틴을 구현해요 (인수는 순서대로 생성되지 않는다는 점에 주의).
sub factors( Int:D \n ) {
my $k = 1;
gather {
while $k**2 < n {
if n %% $k {
take $k;
take n div $k;
}
$k++;
}
take $k if $k**2 == n;
}
}
say factors(36); # OUTPUT: «1, 36, 2, 18, 3, 12, 4, 9, 6»
gather/take 조합은 컨텍스트에 따라 값을 지연 생성할 수 있어요. 스칼라나 sigilless 컨테이너에 바인딩하면 지연성을 강제해요. 지연 평가를 강제하려면 lazy 서브루틴·메서드를 사용해요. 예를 들어:
my @vals = lazy gather {
take 1;
say "Produced a value";
take 2;
}
say @vals[0];
say 'between consumption of two values';
say @vals[1];
# OUTPUT:
# 1
# between consumption of two values
# Produced a value
# 2
gather/take는 동적으로 스코프되므로, gather 안에서 호출되는 subs나 메서드에서 take를 호출할 수 있어요.
sub weird(@elems, :$direction = 'forward') {
my %direction = (
forward => sub { take $_ for @elems },
backward => sub { take $_ for @elems.reverse },
random => sub { take $_ for @elems.pick(*) },
);
return gather %direction{$direction}();
}
say weird(<a b c>, :direction<backward> ); # OUTPUT: «(c b a)»
값이 호출 측에서 변경 가능해야 한다면 take-rw를 사용하세요.
gather/take가 만든 Seq는 다른 타입으로 강제 변환될 수 있다는 점에 주의하세요. hash에 할당하는 예:
my %h = gather { take "foo" => 1; take "bar" => 2};
say %h; # OUTPUT: «{bar => 2, foo => 1}»
참고: gather/take를 react/whenever의 결과를 모으는 데 사용하면 안 돼요. whenever 블록은 gather/react를 실행하는 스레드가 아니라 emit을 실행하는 스레드에서 실행돼요. 이 스레드에는 take가 던지는 제어 예외에 대한 핸들러가 없어서 오류가 나요.
supply/emit
키워드 supply는 tap할 수 있는 on-demand supply인 Supply 객체를 만들어요. supply 접두 코드 어디에서나 쓸 수 있는 emit과 짝을 이뤄요.
emit 메서드 또는 emit 루틴을 사용하면 invocant를 둘러싼 supply에 전달해요.
my $supply = supply {
.emit for "foo", 42, .5;
}
$supply.tap: {
say "received {.^name} ($_)";
}
# OUTPUT:
# received Str (foo)
# received Int (42)
# received Rat (0.5)
given
given 문은 C 같은 언어에서 switch가 토픽화하는 것과 비슷하게 Raku의 토픽화 키워드예요. 즉 given은 다음 블록 안에서 $_를 설정해요. 개별 case의 키워드는 when과 default예요. 보통 관용구는 이렇게 생겼어요.
my $var = (Any, 21, any <answer lie>).pick;
given $var {
when 21 { say $_ * 2 }
when 'lie' { .say }
default { say 'default' }
}
given 문은 종종 단독으로 쓰여요.
given 42 { .say; .Numeric; }
이것은 다음보다 훨씬 이해하기 쉬워요.
{ .say; .Numeric; }(42)
default와 when
default 문을 포함하는 블록은 default 문 뒤의 하위 블록을 떠나면 즉시 그 블록을 떠나요. 블록의 나머지 문들이 건너뛰어진 것처럼요.
given 42 {
"This says".say;
$_ == 42 and ( default { "This says, too".say; 43; } );
"This never says".say;
}
# The above block evaluates to 43
when 문도 이렇게 하지만, when 문장 수식어는 그렇지 않아요.
추가로 when 문은 주제($_)를 제공된 표현식과 smartmatch해, 매치를 지정할 때 값·정규식·타입에 대해 검사할 수 있게 해 줘요.
for 42, 43, "foo", 44, "bar" {
when Int { .say }
when /:i ^Bar/ { .say }
default { say "Not an Int or a Bar" }
}
# OUTPUT: «4243Not an Int or a Bar44Bar»
이 형태에서 given/when 구조는 if/elsif/else 문들의 집합처럼 동작해요. when 문의 순서에 주의하세요. 다음 코드는 42가 아니라 "Int"라고 말해요.
given 42 {
when Int { say "Int" }
when 42 { say 42 }
default { say "huh?" }
}
# OUTPUT: «Int»
when 문이나 default 문이 바깥 블록을 반환하게 하면, 중첩된 when·default 블록은 바깥 블록으로 세지 않으므로 새 블록을 열지 않는 한 이 문들을 중첩하고도 같은 "switch"에 있을 수 있어요.
given 42 {
when Int {
when 42 { say 42 }
say "Int"
}
default { say "huh?" }
}
# OUTPUT: «42»
when 문은 Signature에 대해서도 smartmatch할 수 있어요.
proceed
proceed와 succeed 둘 다 when·default 블록 안에서만 사용하도록 되어 있어요.
proceed 문은 즉시 when·default 블록을 떠나 나머지 문을 건너뛰고 블록 뒤에서 계속해요. 이렇게 하면 when·default가 바깥 블록을 빠져나가지 않게 해 줘요.
given * {
default {
proceed;
"This never says".say
}
}
"This says".say;
이것은 여러 when 블록에 진입할 때 가장 자주 사용돼요. proceed는 성공적인 매치 후 매칭을 다시 시작해요. 이렇게요.
given 42 {
when Int { say "Int"; proceed }
when 42 { say 42 }
when 40..* { say "greater than 40" }
default { say "huh?" }
}
# OUTPUT: «Int»
# OUTPUT: «42»
when 40..* 매치가 일어나지 않았다는 점에 주의하세요. 그런 경우까지 매치하려면 when 42 블록에 proceed가 필요해요.
이것은 C의 switch 문과 같지 않아요. proceed는 바로 다음 블록에만 진입하는 게 아니라 given 값을 한 번 더 매치하려 시도하기 때문이에요. 이 코드를 생각해 보세요.
given 42 {
when Int { "Int".say; proceed }
when 43 { 43.say }
when 42 { 42.say }
default { "got change for an existential answer?".say }
}
# OUTPUT: «Int»
# OUTPUT: «42»
...이것은 Int에 매치하고, 값이 매치되지 않으므로 43을 건너뛰고, 다음 긍정 매치이므로 42에 매치하지만, when 42 블록에 proceed가 없으므로 default 블록에는 진입하지 않아요.
대조적으로 succeed 키워드는 그 지점에서 실행을 단락시키고 전체 given 블록을 빠져나가요. 블록의 최종 값을 지정하는 인자를 받을 수도 있어요.
say do given 42 {
when Int {
succeed "Found";
say "never this!";
}
when 42 { say 42 }
default { say "dunno?" }
}
# OUTPUT: «Found»
when·default 블록 안이 아니라면 proceed·succeed를 쓰려는 것은 오류예요. 또한 when 문장 수식어 형태는 어떤 블록도 떠나게 하지 않고, 그런 문의 succeed·proceed는 있으면 둘러싼 절에 적용돼요.
given 42 {
{ say "This says" } when Int;
"This says too".say;
when * > 41 {
{ "And this says".say; proceed } when * > 41;
"This never says".say;
}
"This also says".say;
}
# OUTPUT: «This saysThis says tooAnd this saysThis also says»
given as a statement (문으로서의 given)
given은 문을 따라와서 그 문의 주제를 설정할 수 있어요.
.say given "foo";
# OUTPUT: «foo»
printf "%s %02i.%02i.%i",
<Mo Tu We Th Fr Sa Su>[.day-of-week - 1],
.day,
.month,
.year
given DateTime.now;
# OUTPUT: «Sa 03.06.2016»
loop
loop 문은 ;로 구분된 세 문(statement)을 괄호 안에 받아 각각 초기화자, 조건, 증가자의 역할을 해요. 초기화자는 조건이 처음 검사되기 전에 한 번 실행돼요. 초기화자가 변수 선언을 포함하면, 그 변수는 루프의 바깥·포함 스코프의 렉시컬 변수로 선언되어 루프 문 뒤의 코드에서 쓸 수 있어요. 조건은 각 반복 전에 실행되고 Bool로 강제 변환돼요. False면 루프가 멈춰요. 증가자는 각 반복 후, 조건이 다시 검사되기 전에 실행돼요.
loop (my $i = 0; $i < 10; $i++) { # A typical loop
say $i;
}
my @str = "However Long".comb; # Our very own .chars routine:
loop (my $l = 0;;) { # Declare $l in outer scope
last if !@str[$l++] # and count chars until we hit
} # an undefined element (Any)
say "The string is {--$l} chars long.";
무한 루프는 괄호가 필요 없어요.
loop { say 'forever' }
loop 문은 목록에 나타나면 붙은 블록의 각 실행 결과에서 값을 만들어낼 수 있어요.
(loop ( my $i = 0; $i++ < 3;) { $i * 2 }).say; # OUTPUT: «(2 4 6)»
my @a = (loop ( my $j = 0; $j++ < 3;) { $j * 2 }); @a.say; # OUTPUT: «[2 4 6]»
my @b = do loop ( my $k = 0; $k++ < 3;) { $k * 2 }; @b.say; # same thing
for 루프와 달리 반환값이 지연 생성되는지에 의존하지 말아야 해요. 반환값이 쓰일 수 있는 루프가 실제로 실행되도록 보장하려면 eager를 쓰는 게 아마 가장 좋아요.
sub heads-in-a-row {
(eager loop (; 2.rand < 1;) { "heads".say })
}
while, until
while 문은 조건이 참인 한 블록을 실행해요. 그래서
my $x = 1;
while $x < 4 {
print $x++;
}
print "\n";
# OUTPUT: «123»
비슷하게 until 문은 표현식이 거짓인 한 블록을 실행해요.
my $x = 1;
until $x > 3 {
print $x++;
}
print "\n";
# OUTPUT: «123»
while·until의 조건은 괄호로 감쌀 수 있지만, 키워드와 조건의 여는 괄호 사이에 공백이 있어야 해요.
while과 until 둘 다 문장 수식어로 쓸 수 있어요. 예:
my $x = 42;
$x-- while $x > 12
next, last 및 관련 항목을 보면 루프 제어를 미세 조정하는 방법을 알 수 있어요. 다른 루프 문법은 repeat/while·repeat/until도 보세요.
이 모든 형태는 loop처럼 반환값을 만들어낼 수 있어요.
repeat/while, repeat/until
블록을 적어도 한 번 실행하고, 조건이 허락하면 그 실행을 반복해요. while/until과 달리 조건이 앞에 나타나더라도 조건은 루프 끝에서 평가돼요.
my $x = -42;
repeat {
$x++;
} while $x < 5;
$x.say; # OUTPUT: «5»
repeat {
$x++;
} while $x < 5;
$x.say; # OUTPUT: «6»
repeat while $x < 10 {
$x++;
}
$x.say; # OUTPUT: «10»
repeat while $x < 10 {
$x++;
}
$x.say; # OUTPUT: «11»
repeat {
$x++;
} until $x >= 15;
$x.say; # OUTPUT: «15»
repeat {
$x++;
} until $x >= 15;
$x.say; # OUTPUT: «16»
repeat until $x >= 20 {
$x++;
}
$x.say; # OUTPUT: «20»
repeat until $x >= 20 {
$x++;
}
$x.say; # OUTPUT: «21»
이 모든 형태는 loop처럼 반환값을 만들어낼 수 있어요.
once
once가 접두된 블록·문은 루프나 재귀 루틴 안에 있어도 정확히 한 번만 실행돼요.
my $guard;
loop {
once $guard = 3;
last if $guard-- <= 0;
once { put 'once' };
print 'many'
} # OUTPUT: «oncemanymanymany»
이것은 포함하는 코드 객체의 "클론"별로 동작하므로:
({ once 42.say } xx 3).map: {$_(), $_()}; # says 42 thrice
같은 블록의 같은 클론이 여러 스레드에서 실행될 때 이건 스레드 안전한 구조가 아니라는 점에 주의하세요. 또한 메서드는 클래스당, 객체당이 아니라 클론을 하나만 가진다는 점을 기억하세요.
LABELs
while, until, loop, for 루프는 모두 라벨을 받을 수 있고, next, last, redo에서 참조할 수 있어요. 중첩 루프가 지원돼요. 예를 들어:
OUTAHERE: while True {
for 1,2,3 -> $n {
last OUTAHERE if $n == 2;
}
}
라벨은 중첩 루프 안에서 각 루프에 이름을 붙이는 데도 쓸 수 있어요. 예를 들어:
OUTAHERE:
loop ( my $i = 1; True; $i++ ) {
OUTFOR:
for 1,2,3 -> $n {
# exits the for loop before its natural end
last OUTFOR if $n == 2;
}
# exits the infinite loop
last OUTAHERE if $i >= 2;
}
next
next 명령은 루프의 다음 반복을 시작해요. 그래서
my @x = 1, 2, 3, 4, 5;
for @x -> $x {
next if $x == 3;
print $x;
}
"1245"를 출력해요.
map에서도 next를 쓸 수 있어요. 위 예는 이렇게 되죠.
my @x = 1, 2, 3, 4, 5;
print @x.map: -> $x {
next if $x == 3;
$x
}
Seq의 항목 사이에 공백이 추가되어 "1 2 4 5"를 출력해요. print가 map의 블록 안에 넣어지지 않은 점에 주의하세요. 일반적으로 map을 부수 효과(이 경우 print)를 위해 실행하는 건 나쁜 관행으로 여겨져요.
NEXT phaser가 있다면 다음 반복 전에 실행돼요.
my Int $i = 0;
while ($i < 10) {
if ($i % 2 == 0) {
next;
}
say "$i is odd.";
NEXT {
$i++;
}
}
# OUTPUT: «1 is odd.3 is odd.5 is odd.7 is odd.9 is odd.»
6.e.PREVIEW 버전(2021.07 Rakudo 컴파일러 릴리스부터 사용 가능)에서는 next 문으로 값을 반환하는 것도 가능해요. map에서 쓸 때 특히 유용해요.
my @x = 1, 2, 3, 4, 5;
print @x.map: -> $x {
next 42 if $x == 3;
$x
}
"1 2 42 4 5"를 출력해요.
whenever 블록에서 next는 현재 값에 대해 즉시 블록을 빠져나와요.
react {
whenever Supply.interval(1) {
next if .is-prime;
say $_;
done if $_ == 4;
}
}
"0", "1", "4"를 출력해요. 즉 소수를 건너뛴 0~4의 정수예요.
6.d 버전부터, 마지막 문 값을 모으는 루프에서 next 명령은 실행되는 반복에 대해 Empty를 반환해요.
last
last 명령은 즉시 해당 루프를 빠져나가요.
my @x = 1, 2, 3, 4, 5;
for @x -> $x {
last if $x == 3;
print $x;
}
"12"를 출력해요.
map에서도 last를 쓸 수 있어요. 위 예는 이렇게 되죠.
my @x = 1, 2, 3, 4, 5;
print @x.map: -> $x {
last if $x == 3;
$x
}
Seq의 항목 사이에 공백이 추가되어 "1 2"를 출력해요. print가 map의 블록 안에 넣어지지 않은 점에 주의하세요. 일반적으로 map을 부수 효과(이 경우 print)를 위해 실행하는 건 나쁜 관행으로 여겨져요.
LAST phaser가 있다면 루프를 빠져나가기 전에 실행돼요.
my Int $i = 1;
while ($i < 10) {
if ($i % 5 == 0) {
last;
}
LAST {
say "The last number was $i.";
}
NEXT {
$i++;
}
}
# OUTPUT: «The last number was 5.»
6.d 버전부터, 마지막 문 값을 모으는 루프에서 last 명령은 실행되는 반복에 대해 Empty를 반환해요.
6.e.PREVIEW 버전(2021.07 Rakudo 컴파일러 릴리스부터 사용 가능)에서는 last 문으로 값을 반환하는 것도 가능해요. map에서 쓸 때 특히 유용해요.
my @x = 1, 2, 3, 4, 5;
print @x.map: -> $x {
last 42 if $x == 3;
$x
}
"1 2 42"를 출력해요.
redo
redo 명령은 조건을 다시 평가하지 않고 루프 블록을 재시작해요.
for 1..5 -> $current-level {
state $total-attempts = 0;
$total-attempts++;
print("Entering #$current-level. ");
if $total-attempts %% 3 {
redo;
}
}
# OUTPUT: «Entering #1... Entering #2... Entering #3... Entering #3... Entering #4... Entering #5... Entering #5... »
return
sub return은 서브루틴·메서드의 실행을 멈추고 관련 phaser들을 모두 실행한 뒤 주어진 반환값을 호출자에게 제공해요. 기본 반환값은 Nil이에요. 반환값이 Nil이 아닌 한 반환 타입 제약이 제공되면 검사돼요. 타입 검사가 실패하면 X::TypeCheck::Return 예외가 던져져요. 통과하면 제어 예외가 발생하고 CONTROL로 잡을 수 있어요.
블록의 어떤 return도, 얼마나 깊이 중첩됐든 그 블록의 바깥 렉시컬 스코프에 있는 첫 Routine에 묶여요. 패키지의 루트에 있는 return은 런타임에 실패한다는 점에 주의하세요. 지연 평가되는 블록(예: map 안)의 return은 블록이 실행될 때쯤엔 바깥 렉시컬 루틴이 사라진 걸 발견할 수 있어요. 거의 모든 경우 last가 더 나은 대안이에요. 반환값이 어떻게 처리·생성되는지에 대한 자세한 내용은 함수 문서를 확인하세요.
return-rw
sub return은 컨테이너가 아니라 값을 반환해요. 그 값들은 불변이고, 변경을 시도하면 런타임 오류로 이어져요.
sub s(){ my $a = 41; return $a };
say ++s();
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Multi::NoMatch.new(dispatcher …
변경 가능한 컨테이너를 반환하려면 return-rw를 쓰세요.
sub s(){ my $a = 41; return-rw $a };
say ++s();
# OUTPUT: «42»
return과 동일한 phaser·제어 예외 규칙이 적용돼요.
fail
현재 루틴을 떠나 관련 phaser들을 모두 실행한 뒤, 제공된 Exception이나 Str을 Failure 안에 감싼 것을 반환해요. 호출자가 use fatal; 프라그마로 치명적 예외를 활성화했다면, 예외는 Failure로 반환되는 대신 던져져요.
sub f { fail "WELP!" };
say f;
CATCH { default { say .^name, ': ', .Str } }
# OUTPUT: «X::AdHoc: WELP!»