피해야 할 함정(Traps)

피해야 할 함정(Traps) (traps)

프로그래밍 언어를 배울 때, 특히 다른 언어에 익숙한 상태에서 Raku를 배울 때, 항상 나를 놀라게 하고 디버깅과 발견에 소중한 시간을 잃게 만드는 것들이 있어요. 이 문서는 흔한 오해들을 보여 줘서 그런 함정을 피하게 해 주는 것이 목적이에요.

출처: Raku Docs - Traps to avoid

Raku를 만들 때 구문의 결점(warts)을 제거하는 데 큰 공을 들였어요. 그런데 결점 하나를 때려 없애면 때로는 다른 게 튀어나와요. 그래서 결점의 최소 개수를 찾거나, 좀처럼 보이지 않는 곳에 배치하는 데 많은 시간을 보냈죠. 그 때문에 Raku의 결점들은 다른 언어에서 왔다면 기대하는 곳과는 다른 곳에 있어요.

변수와 상수

상수는 컴파일 타임에 계산된다

상수는 컴파일 타임에 계산돼요. 그래서 모듈 안에서 상수를 쓰면, 모듈 자체의 사전 컴파일(precompilation) 때문에 그 값이 얼어붙는다는 점을 명심하세요.

# WRONG (most likely):
unit module Something::Or::Other;
constant $config-file = "config.txt".IO.slurp;

$config-file은 사전 컴파일 중에 slurp되고, config.txt 파일이 바뀌어도 스크립트를 다시 시작할 때 다시 로드되지 않아요. 모듈이 다시 컴파일될 때만 반영되죠.

컨테이너 사용을 피하고, 상수와 비슷한 동작을 하면서도 값이 갱신될 수 있는 변수에 값을 바인딩하는 것을 선호하세요.

# Good; file gets updated from 'config.txt' file on each script run:
unit module Something::Or::Other;
my $config-file := "config.txt".IO.slurp;

Nil 할당은 다른 값, 보통 Any를 만들 수 있다

실제로 변수에 Nil을 할당하면 변수가 기본값으로 되돌아가요. 예를 들어

my @a = 4, 8, 15, 16;
@a[2] = Nil;
say @a; # OUTPUT: «[4 8 (Any) 16]␤»

이 경우 AnyArray 요소의 기본값이에요. 의도적으로 기본값으로 Nil을 할당할 수도 있어요.

my %h is default(Nil) = a => Nil;
say %h; # OUTPUT: «Hash %h = {:a(Nil)}␤»

또는 그런 결과를 원한다면 값을 Nil에 바인딩하세요.

@a[3] := Nil;
say @a; # OUTPUT: «[4 8 (Any) Nil]␤»

이 함정은 매치 같은 함수의 결과에 숨어 있을 수 있어요.

my $result2 = 'abcdef' ~~ / dex /;
say "Result2 is { $result2.^name }"; # OUTPUT: «Result2 is Any␤»

Match는 아무것도 찾지 못하면 Nil이 돼요. 하지만 위에서 $result2Nil을 할당하면 그 기본값, 즉 보이는 것처럼 Any가 돼요.

블록으로 익명 상태 변수를 보간하기

프로그래머는 루틴이 호출된 횟수를 세고 싶었는데, 카운터가 증가하지 않아요.

sub count-it { say "Count is {$++}" }
count-it;
count-it;

# OUTPUT:
# Count is 0
# Count is 0

상태 변수에 관해서, 변수가 선언된 블록은 그 블록의 블록이 다시 진입될 때마다 복제되고 —변수는 새로 초기화되고— 돼요. 덕분에 아래와 같은 구성이 적절하게 동작해요. 루프 안의 상태 변수는 sub가 호출될 때마다 새로 초기화되죠.

sub count-it {
    for ^3 {
        state $count = 0;
        say "Count is $count";
        $count++;
    }
}

count-it;
say "…and again…";
count-it;


# OUTPUT:
# Count is 0
# Count is 1
# Count is 2
# …and again…
# Count is 0
# Count is 1
# Count is 2

같은 배치가 우리의 버그 있는 프로그램에도 있어요. 큰따옴표 문자열 안의 { }는 단순히 코드 조각을 실행하는 보간이 아니에요. 실제로 그것 자체가 블록이고, 위 예시처럼 sub가 진입될 때마다 복제되어 상태 변수를 다시 초기화해요. 올바른 카운트를 얻으려면 그 내부 블록을 없애고, 코드 조각을 보간하는 데 스칼라 맥락화자(scalar contextualizer)를 써야 해요.

sub count-it { say "Count is $($++)" }
count-it;
count-it;

# OUTPUT:
# Count is 0
# Count is 1

대안으로 연결 연산자를 쓸 수도 있어요.

sub count-it { say "Count is " ~ $++ }

값이 falsy일 때 Associative에 집합 서브루틴 사용하기

Associative를 구현하는 클래스에 (cont), , , (elem), , 을 쓰면, 키의 값이 falsy일 때 False를 돌려줘요.

enum Foo «a b»;
say Foo.enums ∋ 'a';

# OUTPUT:
# False

대신 :exists를 쓰세요.

enum Foo «a b»;
say Foo.enums<a>:exists;

# OUTPUT:
# True

블록

빈 "블록" 조심하기

중괄호는 블록을 선언하는 데 쓰여요. 하지만 빈 중괄호는 해시를 선언해요.

$ = {say 42;} # Block
$ = {;}       # Block
$ = {…}       # Block
$ = { }       # Hash

실제로 빈 블록을 선언하고 싶다면 두 번째 형태를 쓸 수 있어요.

my &does-nothing = {;};
say does-nothing(33); # OUTPUT: «Nil␤»

객체

속성에 할당하기

새로 온 사람들은 접근자가 있는 속성이 has $.x로 선언되니까 클래스 안에서 $.x에 할당할 수 있다고 자주 생각해요. 그렇지 않아요.

예를 들어

class Point {
    has $.x;
    has $.y;
    method double {
        $.x *= 2;   # WRONG
        $.y *= 2;   # WRONG
        self;
    }
}

say Point.new(x => 1, y => -2).double.x
# OUTPUT: «Cannot assign to an immutable value␤»

메서드 double 안의 첫 줄이 # WRONG으로 표시된 이유는 $.x$( self.x )의 줄임말로, 읽기 전용 접근자를 호출하는 것이기 때문이에요. has $.x 문법은 has $!x; method x() { $!x } 같은 것의 줄임말이라서, 실제 속성은 $!x라고 불리고 읽기 전용 접근자 메서드가 자동으로 생성돼요.

그래서 메서드 double을 올바르게 쓰는 방법은

method double {
    $!x *= 2;
    $!y *= 2;
    self;
}

처럼 속성을 직접 다루는 것이에요.

BUILD는 생성자 인자에서 자동 속성 초기화를 막는다

자신만의 BUILD 서브메서드를 정의하면 모든 속성을 직접 초기화해야 해요. 예를 들어

class A {
    has $.x;
    has $.y;
    submethod BUILD {
        $!y = 18;
    }
}

say A.new(x => 42).x;       # OUTPUT: «Any␤»

$!x를 초기화하지 않은 채 남겨요. 커스텀 BUILD가 그것을 초기화하지 않기 때문이죠.

참고: TWEAK을 고려해 보세요. Rakudo는 2016.11 릴리스부터 TWEAK 메서드를 지원해요.

가능한 해결책 하나는 속성을 BUILD에서 명시적으로 초기화하는 것이에요.

submethod BUILD(:$x) {
    $!y = 18;
    $!x := $x;
}

이건 이렇게 줄일 수 있어요.

submethod BUILD(:$!x) {
    $!y = 18;
}

공백

정규식의 공백은 문자 그대로 일치하지 않는다

say 'a b' ~~ /a b/; # OUTPUT: «False␤»

정규식의 공백은 기본적으로 Raku 언어의 나머지와 마찬가지로 의미 없는 선택적 채움으로 간주돼요. 공백을 일치시키는 방법:

  • \s는 임의의 공백 하나, \s+는 최소 하나
  • ' '(따옴표 안의 빈칸)은 빈칸 하나
  • \t, \n은 특정 공백(탭, 새 줄)
  • \h, \v는 가로, 세로 공백
  • .ws는 공백을 위한 내장 규칙으로, 여러분이 실제로 원하는 동작을 하는 경우가 많음
  • m:s/a b/ 또는 m:sigspace/a b/로 정규식의 빈칸이 임의의 공백과 일치하게 할 수 있음

파싱의 모호성

어떤 언어는 토큰 사이의 공백을 최대한 제거해도 괜찮게 해 주지만, Raku는 그렇게 관대하지 않아요. 가장 중요한 모토는 우리가 코드 골프를 권장하지 않는다는 것, 그러니 공백을 아끼지 말라는 것이에요. (이런 제한 뒤의 더 진지한 근본 이유는 단일 패스 파싱과, 백트래킹이 거의 없는 Raku 프로그램 파싱 능력 때문이에요.) 주의할 흔한 영역은 다음과 같아요.

블록 vs. 해시 슬라이스 모호성

# WRONG; trying to hash-slice a Bool:
while ($++ > 5){ .say }
# RIGHT:
while ($++ > 5) { .say }

# EVEN BETTER; Raku does not require parentheses there:
while $++ > 5 { .say }

축약 vs. 배열 생성자 모호성

# WRONG; ambiguity with `[<]` metaop:
my @a = [[<foo>],];
# RIGHT; reductions cannot have spaces in them, so put one in:
my @a = [[ <foo>],];

# No ambiguity here, natural spaces between items suffice to resolve it:
my @a = [[<foo bar ber>],];

미만 vs. 단어 인용/연관 인덱싱

# WRONG; trying to index 3 associatively:
say 3<5>4
# RIGHT; prefer some extra whitespace around infix operators:
say 3 < 5 > 4

배타적 시퀀스 vs. Range가 있는 시퀀스

...^ 연산자가 ... 연산자 뒤에 ^ 연산자가 바로 오는 것으로 오인될 수 있는 방법에 대한 자세한 내용은 연산자 함정 섹션을 참고하세요. 어떤 해석이 따르게 할 것인지 나타내려면 공백을 올바르게 사용해야 해요.

캡처(Captures)

캡처 안의 컨테이너 대 값

초보자는 Capture 안의 변수가 나중에 그 Capture가 사용될 때 현재 값을 공급할 것이라 기대할 수 있어요. 예를 들어

my $a = 2; say join ",", ($a, ++$a);  # OUTPUT: «3,3␤»

여기서 Capture$a가 가리키는 컨테이너와 표현식 ++$a의 결과의 을 담고 있어요. Capture&say가 사용하기 전에 구체화(reify)되어야 하므로, ++$a&say$a의 컨테이너를 들여다보기 전에(그리고 두 term으로 List가 만들어지기 전에) 일어날 수 있어서 이미 증가되어 있을 수 있어요. 대신 값을 원하면 값을 만들어 내는 표현식을 사용하세요.

my $a = 2; say join ",", (+$a, ++$a); # OUTPUT: «2,3␤»

아니면 더 간단하게

my $a = 2; say  "$a, {++$a}"; # OUTPUT: «2, 3␤»

이 경우에도 똑같이 일어나요.

my @arr;
my ($a, $b) = (1,1);
for ^5 {
    ($a,$b) = ($b, $a+$b);
    @arr.push: ($a, $b);
    say @arr
};

«[(1 2)]␤[(2 3) (2 3)]␤[(3 5) (3 5) (3 5)]␤...를 출력해요. $a$bsay가 호출될 때까지 구체화되지 않아서, 그 정확한 순간에 가진 값이 출력돼요. 이를 피하려면 값을 디컨테이너화하거나, 사용하기 전에 어떤 방식으로든 변수에서 꺼내세요.

my @arr;
my ($a, $b) = (1,1);
for ^5 {
    ($a,$b) = ($b, $a+$b);
    @arr.push: ($a.item, $b.item);
    say @arr
};

item으로 컨테이너가 item 컨텍스트에서 평가되어 값이 추출되고, 원하는 결과를 얻어요.

Cool 트릭

Raku에는 Cool 클래스가 있어서, 필요할 때 인자를 강제 변환함으로써 우리가 익숙해진 DWIM 동작의 일부를 제공해요. 하지만 DWIM은 결코 완벽하지 않아요. 특히 CoolList에는 contains, starts-with, index 같은, 여러분이 생각하는 대로 동작하지 않을 많은 메서드가 있어요. 아래 섹션에서 몇 가지 예를 보세요.

문자열은 List가 아니므로 인덱싱에 주의

Raku에서 문자열(Str)은 문자의 목록이 아니에요. .index 메서드의 이름에도 불구하고, List처럼 문자열을 순회하거나 인덱싱할 수 없어요.

List는 문자열이 되므로 .index()에 주의

List.index에 접근을 제공하는 Cool에서 상속받아요. .indexListStr강제 변환하는 방식 때문에, 때로는 목록에서 요소의 인덱스를 돌려주는 것처럼 보일 수 있지만 그런 방식으로 동작이 정의된 게 아니에요.

my @a = <a b c d>;
say @a.index(‘a’);    # OUTPUT: «0␤»
say @a.index('c');    # OUTPUT: «4␤» -- not 2!
say @a.index('b c');  # OUTPUT: «2␤» -- not undefined!
say @a.index(<a b>);  # OUTPUT: «0␤» -- not undefined!

이 같은 주의 사항이 .rindex에도 적용돼요.

List는 문자열이 되므로 .contains()에 주의

비슷하게 .contains는 목록에서 요소를 찾지 않아요.

my @menu = <hamburger fries milkshake>;
say @menu.contains('hamburger');            # OUTPUT: «True␤»
say @menu.contains('hot dog');              # OUTPUT: «False␤»
say @menu.contains('milk');                 # OUTPUT: «True␤»!
say @menu.contains('er fr');                # OUTPUT: «True␤»!
say @menu.contains(<es mi>);                # OUTPUT: «True␤»!

실제로 요소의 존재를 검사하려면 단일 요소에는 (cont) 연산자를, 여러 요소에는 supersetstrict superset 연산자를 사용하세요.

my @menu = <hamburger fries milkshake>;
say @menu (cont) 'fries';                   # OUTPUT: «True␤»
say @menu (cont) 'milk';                    # OUTPUT: «False␤»
say @menu (>) <hamburger fries>;            # OUTPUT: «True␤»
say @menu (>) <milkshake fries>;            # OUTPUT: «True␤» (! NB: order doesn't matter)

요소 테스트를 많이 한다면 Set을 쓰는 편이 나을 수 있어요.

Numeric 리터럴은 강제 변환 전에 파싱된다

경험 많은 프로그래머라면 놀라지 않겠지만, Numeric 리터럴은 문자열로 강제 변환되기 전에 그 수치 값으로 파싱돼서 비직관적인 결과를 만들 수 있어요.

say 0xff.contains(55);      # OUTPUT: «True␤»
say 0xff.contains(0xf);     # OUTPUT: «False␤»
say 12_345.contains("23");  # OUTPUT: «True␤»
say 12_345.contains("2_");  # OUTPUT: «False␤»

List에서 임의 항목 얻기

컬렉션에서 무작위 요소 하나 이상을 가져오는 것은 흔한 작업이지만, List.rand는 그런 방법이 아니에요. Coolrand를 제공하지만, 그것은 먼저 List를 목록의 항목 수로 강제 변환하고, 0과 그 값 사이의 무작위 실수를 돌려줘요. 무작위 요소를 얻으려면 pickroll을 보세요.

my @colors = <red orange yellow green blue indigo violet>;
say @colors.rand;       # OUTPUT: «2.21921955680514␤»
say @colors.pick;       # OUTPUT: «orange␤»
say @colors.roll;       # OUTPUT: «blue␤»
say @colors.pick(2);    # OUTPUT: «(yellow violet)␤»  (cannot repeat)
say @colors.roll(3);    # OUTPUT: «(red green red)␤»  (can repeat)

List은 수치 컨텍스트에서 요소 수로 numify된다

숫자가 숫자 집합 중 하나로 나누어지는지 검사하고 싶다고 해 볼게요.

say 42 %% <11 33 88 55 111 20325>; # OUTPUT: «True␤»

뭐라고요? 42를 나누는 단일 숫자는 없잖아요. 하지만 그 목록은 6개 요소를 갖고, 42는 6으로 나누어져요. 그래서 출력이 true인 거예요. 이 경우 ListJunction으로 바꿔야 해요.

say 42 %% <11 33 88 55 111 20325>.any;
# OUTPUT: «any(False, False, False, False, False, False)␤»

이러면 각각 개별적으로 numify되는 목록의 모든 숫자의 나눗셈 불가 여부가 분명히 드러나요.

배열

배열의 마지막 요소 참조하기

어떤 언어에서는 배열의 "-1번째" 요소를 요청해 마지막 요소를 참조할 수 있어요. 예:

my @array = qw{victor alice bob charlie eve};
say @array[-1];    # OUTPUT: «eve␤»

Raku에서는 음수 첨자를 쓸 수 없지만, 함수(*-1)를 써서 같은 결과를 얻어요. 그래서 배열의 마지막 요소에 접근하는 것은 이렇게 되죠.

my @array = qw{victor alice bob charlie eve};
say @array[*-1];   # OUTPUT: «eve␤»

또 다른 방법은 배열의 tail 메서드를 활용하는 것이에요.

my @array = qw{victor alice bob charlie eve};
say @array.tail;      # OUTPUT: «eve␤»
say @array.tail(2);   # OUTPUT: «(charlie eve)␤»

타입화된 배열 파라미터

새 사용자들은 문서를 충분히 읽기 전에 이런 걸 자주 쓰게 돼요.

sub foo(Array @a) { ... }

이것은 배열의 배열(Array of Arrays)을 요구하는 것이라는 사실을 깨닫기 전에요. @a가 오직 Array만 받아들이게 하려면 대신 이렇게 쓰세요.

sub foo(@a where Array) { ... }

이 또한 동작할 것이라 기대하는 경우가 흔한데, 실제로는 안 돼요.

sub bar(Int @a) { 42.say };
bar([1, 2, 3]);             # expected Positional[Int] but got Array

여기 문제는 [1, 2, 3]이 Array[Int]가 아니라는 거예요. 그냥 우연히 Int가 들어 있는 평범한 Array죠. 동작하게 하려면 인자도 Array[Int]여야 해요.

my Int @b = 1, 2, 3;
bar(@b);                    # OUTPUT: «42␤»
bar(Array[Int].new(1, 2, 3));

불편해 보일 수 있지만, 장점은 @b에 무엇이 할당되는지에 대한 타입 검사가 매 호출마다 모든 요소를 검사하는 대신 할당이 일어나는 곳으로 옮겨진다는 점이에요.

필요 없을 때 «» 인용 사용하기

이 함정은 여러 변종으로 나타나요. 몇 가지를 볼게요.

my $x = ‘hello’;
my $y = ‘foo bar’;

my %h = $x => 42, $y => 99;
say %h«$x»;   # ← WRONG; assumption that $x has no whitespace
say %h«$y»;   # ← WRONG; splits ‘foo bar’ by whitespace
say %h«"$y"»; # ← KINDA OK; it works but there is no good reason to do that
say %h{$y};   # ← RIGHT; this is what should be used

run «touch $x»;        # ← WRONG; assumption that only one file will be created
run «touch $y»;        # ← WRONG; will touch file ‘foo’ and ‘bar’
run «touch "$y"»;      # ← WRONG; better, but has a different issue if $y starts with -
run «touch -- "$y"»;   # ← KINDA OK; it works but there is no good enough reason to do that
run ‘touch’, ‘--’, $y; # ← RIGHT; explicit and *always* correct
run <touch -->, $y;    # ← RIGHT; < > are OK, this is short and correct

기본적으로 «» 인용은 변수를 항상 인용한다는 것을 기억해야만 안전하게 쓸 수 있어요. 문제는 그것이 기본 동작을 안전하지 않은 변형으로 뒤집어서, 인용 몇 개를 잊는 것만으로 버그나 심지어 보안 허점을 도입할 위험이 생겨요. 안전하게 가려면 «»을 쓰지 마세요.

문자열

Str을 다룰 때 생길 수 있는 몇 가지 문제예요.

인용과 보간

문자열 리터럴의 보간은 너무 영리해서 오히려 나쁠 수 있어요.

# "HTML tags" interpreted as associative indexing:
"$foo<html></html>" eq
"$foo{'html'}{'/html'}"

# Parentheses interpreted as call with argument:
"$foo(" ~ @args ~ ")" eq
"$foo(' ~ @args ~ ')"

보간하지 않는 작은따옴표를 쓰고 \qq[] 이스케이프 시퀀스로 더 자유로운 보간으로 전환하면 이런 문제를 피할 수 있어요.

my $a = 1;
say '\qq[$a]()$b()';
# OUTPUT: «1()$b()␤»

또 다른 대안은 Q:c 인용자를 쓰고 모든 보간에 코드 블록 {}을 사용하는 것이에요.

my $a = 1;
say Q:c«{$a}()$b()»;
# OUTPUT: «1()$b()␤»

qqx 안에서 쓰이는 변수에 주의

qqx[] 안의 변수는 보안 허점을 도입할 수 있어요. 변수 내용을 교묘하게 만든 문자열로 설정해 임의 코드를 실행할 수 있죠.

my $world = "there\";rm -rf /path/to/dir\"";
say qqx{echo "hello $world"};
# OUTPUT: «hello there␤»

위 코드는 _/path/to/dir_를 지우기도 해요. 변수 내용에 셸 특수 문자가 없게 하거나, 외부 명령을 실행하는 더 좋은 방법으로 runProc::Async를 쓰면 이 문제를 피할 수 있어요.

문자열은 순회할 수 없다

StrAny에서 상속받는 메서드 중 목록 같은 순회 가능한 것에서 동작하는 것들이 있어요. 문자열의 반복자는 문자열 전체인 요소 하나만을 포함해요. sort, reverse 같은 목록 기반 메서드를 쓰려면 먼저 문자열을 목록으로 변환해야 해요.

say "cba".sort;              # OUTPUT: «(cba)␤»
say "cba".comb.sort.join;    # OUTPUT: «abc␤»

.chars는 코드포인트가 아니라 그래픽(graphemes) 수를 얻는다

Raku에서 .chars는 그래픽, 즉 사용자에게 보이는 문자의 수를 돌려줘요. 이 그래픽들은 예를 들어 문자에 악센트가 붙은 것으로 이루어질 수 있어요. 코드포인트 수가 필요하면 .codes를 쓰세요. UTF8로 인코딩했을 때 바이트 수가 필요하면 .encode.bytes로 문자열을 UTF8로 인코딩한 뒤 바이트 수를 얻으면 돼요.

say "\c[LATIN SMALL LETTER J WITH CARON, COMBINING DOT BELOW]"; # OUTPUT: «ǰ̣␤»
say 'ǰ̣'.codes;        # OUTPUT: «2␤»
say 'ǰ̣'.chars;        # OUTPUT: «1␤»
say 'ǰ̣'.encode.bytes; # OUTPUT: «4␤»

Raku에서 문자열이 어떻게 동작하는지 더 알고 싶다면 Unicode 페이지를 참고하세요.

모든 텍스트는 기본적으로 정규화된다

Raku는 모든 텍스트를 Unicode NFC 형식(Normalization Form Canonical)으로 정규화해요. 파일 이름만 기본적으로 정규화되지 않는 유일한 텍스트예요. 문자열이 원본과 바이트 단위로 그대로 유지되기를 기대한다면, 파일핸들을 읽거나 쓸 때 UTF8-C8을 사용해야 해요.

Allomorph는 일반적으로 수치 의미론을 따른다

Str "0"True인 반면 NumericFalse예요. 그럼 allomorph <0>Bool 값은 뭘까요?

일반적으로 allomorph는 Numeric 의미론을 따르므로, 수치적으로 0으로 평가되는 것들은 False예요.

say so   <0>; # OUTPUT: «False␤»
say so <0e0>; # OUTPUT: «False␤»
say so <0.0>; # OUTPUT: «False␤»

allomorph의 Stringy 부분에 대한 비교를 강제하려면 prefix ~ 연산자Str 메서드로 allomorph를 Str로 강제 변환하거나, chars 루틴으로 allomorph에 길이가 있는지 검사하세요.

say so      ~<0>;     # OUTPUT: «True␤»
say so       <0>.Str; # OUTPUT: «True␤»
say so chars <0>;     # OUTPUT: «True␤»

문자열의 대소문자 무시 비교

대소문자 무시 비교를 하려면 .fc(fold-case)를 쓸 수 있어요. 문제는 사람들이 .lc.uc를 쓰는 경향이 있는데, ASCII 범위에서는 동작하는 것처럼 보여도 다른 문자에서는 실패한다는 것이에요. 이것은 Raku만의 함정이 아니에요. 다른 언어에도 똑같이 적용돼요.

say ‘groß’.lc eq ‘GROSS’.lc; # ← WRONG; False
say ‘groß’.uc eq ‘GROSS’.uc; # ← WRONG; True, but that's just luck
say ‘groß’.fc eq ‘GROSS’.fc; # ← RIGHT; True

정규식으로 작업한다면 .fc를 쓸 필요 없이 :i(:ignorecase) 부사를 쓰면 돼요.

Pair

페어 표기의 왼쪽에 있는 상수

이 코드를 생각해 보세요.

enum Animals <Dog Cat>;
my %h := :{ Dog => 42 };
say %h{Dog}; # OUTPUT: «(Any)␤»

:{ … } 문법은 객체 해시를 만들기 위해 쓰여요. 이 코드를 쓴 사람의 의도는 Enum 객체를 키로 하는 해시를 만들고자 한 거였어요 (say %h{Dog}는 Enum 객체로 조회를 수행해 값을 얻으려는 시도). 하지만 페어 표기는 그렇게 동작하지 않아요.

예를 들어 Dog => 42에서 키는 Str이 돼요. 같은 이름의 상수나 열거형이 있어도 상관없어요. 페어 표기는 왼쪽이 식별자처럼 보이는 한 항상 왼쪽을 문자열 리터럴로 사용해요. 이를 피하려면 (Dog) => 42 또는 ::Dog => 42를 쓰세요.

Pair 안의 스칼라 값

Scalar 값을 다룰 때, Pair는 값의 컨테이너를 보유해요. 이는 Pair 외부에서 Scalar 값의 변경을 반영할 수 있다는 뜻이에요.

my $v = 'value A';
my $pair = Pair.new( 'a', $v );
$pair.say;  # OUTPUT: a => value A

$v = 'value B';
$pair.say; # OUTPUT: a => value B

freeze 메서드를 써서 Pair에서 Scalar 컨테이너 제거를 강제해요. 자세한 내용은 Pair 문서를 참고하세요.

세트, 백, 믹스

세트, 백, 믹스는 고정된 순서가 없다

이런 종류의 객체를 순회할 때 순서는 정의되지 않아요.

my $set = <a b c>.Set;
.say for $set.list; # OUTPUT: «a => True␤c => True␤b => True␤»
# OUTPUT: «a => True␤c => True␤b => True␤»
# OUTPUT: «c => True␤b => True␤a => True␤»

매 순회마다 다른 순서가 생길 수 있고(그리고 생길 거고), 그래서 세트 요소의 특정 순서를 신뢰할 수 없어요. 순서가 중요하지 않다면 그냥 그렇게 써요. 중요하다면 sort를 쓰세요.

my $set = <a b c>.Set;
.say for $set.list.sort;  # OUTPUT: «a => True␤b => True␤c => True␤»

일반적으로 세트·백·믹스는 순서가 없으므로, 특정 순서를 가질 것에 의존하면 안 돼요.

연산자

다른 언어에서 흔히 공유하는 일부 연산자가 Raku에서는 다른, 더 흔한 것들로 용도가 바뀌었어요.

Junction

^, |, &비트 연산자가 아니고, Junction을 만들어요. Raku에서 대응하는 비트 연산자는 정수용 +^, +|, +&, 그리고 Bool?^, ?|, ?&이에요.

배타적 시퀀스 연산자

공백을 넉넉히 쓰면 가독성에 좋지만, 중위 연산자 안에는 공백이 있을 수 없다는 점을 기억하세요. 그런 연산자 중 하나가 오른쪽 끝점을 제외하는 시퀀스 연산자 ...^(또는 Unicode 동격 …^)이에요.

say 1... ^5; # OUTPUT: «(1 0 1 2 3 4)␤»
say 1...^5;  # OUTPUT: «(1 2 3 4)␤»

줄임표()와 캐럿(^) 사이에 공백을 두면, 그것은 더 이상 단일 중위 연산자가 아니라 내포적 시퀀스 연산자()와 접두 Range 연산자(^)가 돼요. Iterable은 시퀀스 연산자의 유효한 끝점이므로, 얻는 결과가 기대와 다를 수 있어요.

문자열 범위/시퀀스

어떤 언어에서 문자열을 범위의 끝점으로 쓰면, 다음 문자열이 무엇이어야 하는지 알아낼 때 전체 문자열을 고려해요. 문자열을 큰 진법의 숫자처럼 느슨하게 취급하는 거죠. 다음은 Perl 버전이에요.

say join ", ", "az".."bc";
# OUTPUT: «az, ba, bb, bc␤»

Raku에서 그런 범위는 다른 결과를 만들어요. _각 문자_가 끝점의 대응하는 문자로 범위가 정해져서 더 복잡한 시퀀스를 만들죠.

say join ", ", "az".."bc";
#`{ OUTPUT: «
    az, ay, ax, aw, av, au, at, as, ar, aq, ap, ao, an, am, al, ak, aj, ai, ah,
    ag, af, ae, ad, ac, bz, by, bx, bw, bv, bu, bt, bs, br, bq, bp, bo, bn, bm,
    bl, bk, bj, bi, bh, bg, bf, be, bd, bc
␤»}
say join ", ", "r2".."t3";
# OUTPUT: «r2, r3, s2, s3, t2, t3␤»

위 Perl 예시와 비슷한 더 단순한 동작을 얻으려면, 시작 문자열에서 .succ 메서드를 호출하는 시퀀스 연산자를 사용하세요.

say join ", ", ("az", *.succ ... "bc");
# OUTPUT: «az, ba, bb, bc␤»

주제화(topicalizing) 연산자

스마트매치 연산자 ~~andthen은 topic $_을 그들의 왼쪽으로 설정해요. topic에 대한 암시적 메서드 호출과 결합하면 놀라운 결과를 낼 수 있어요.

my &method = { note $_; $_ };
$_ = 'object';
say .&method;
# OUTPUT: «object␤object␤»
say 'topic' ~~ .&method;
# OUTPUT: «topic␤True␤»

많은 경우 메서드 호출을 왼쪽으로 뒤집으면 동작해요.

my &method = { note $_; $_ };
$_ = 'object';
say .&method;
# OUTPUT: «object␤object␤»
say .&method ~~ 'topic';
# OUTPUT: «object␤False␤»

Fat arrow와 상수

fat arrow 연산자 =>는 상수나 \-시길 변수를 위해 스코프를 확인하지 않고 왼쪽의 단어를 Str로 바꿔요. 의도한 바를 얻으려면 명시적 스코핑을 사용하세요.

constant V = 'x';
my %h = V => 'oi‽', ::V => 42;
say %h.raku
# OUTPUT: «{:V("oi‽"), :x(42)}␤»

중위 연산자 할당

중위 연산자는(내장이든 사용자 정의든) 이 덧셈 예시가 보여주듯 할당 연산자와 결합될 수 있어요.

my $x = 10;
$x += 20;
say $x;     # OUTPUT: «30␤»

어떤 중위 연산자 op에 대해 L op= RL = L op R와 동등해요(여기서 LR은 각각 왼쪽·오른쪽 인자). 이는 다음 코드가 기대대로 동작하지 않을 수 있다는 뜻이에요.

my @a = 1, 2, 3;
@a += 10;
say @a;  # OUTPUT: «[13]␤»

C++ 같은 언어에서 왔다면 이상해 보일 수 있어요. +=가 왼쪽 인자(여기서는 @a 배열)의 메서드로 정의된 것이 아니라 단순히 이것의 줄임말이라는 점을 명심하는 것이 중요해요.

my @a = 1, 2, 3;
@a = @a + 10;
say @a;  # OUTPUT: «[13]␤»

여기서 @a는 (세 요소를 가진) @a10을 더한 결과가 할당돼요. 그래서 13@a에 들어가요. 대신 할당 연산자의 하이퍼 형태를 사용하세요.

my @a = 1, 2, 3;
@a »+=» 10;
say @a;  # OUTPUT: «[11 12 13]␤»

메서드 호출은 체이닝되지 않는다

위의 L = L op R 설명에 대한 예외는 중위 연산자 할당이 메서드 호출 연산자(L .= R)와 결합될 때 발생해요. 이 경우 체인의 첫 번째 메서드만 할당에 적용돼요.

my $s = "abcd";
say $s .= uc.lc; # OUTPUT: abcd
say $s;          # OUTPUT: ABCD

체인을 개별 중위 연산자 할당으로 나누면 원하는 효과를 얻을 수 있어요.

my $s = "abcd";
say $s .= uc .= lc; # OUTPUT: abcd
say $s;             # OUTPUT: abcd

정규식

$x vs <$x>, $(code) vs <{code}>

Raku는 보간을 통해 런타임에 정규식을 생성하는 여러 구조를 제공해요(자세한 설명은 여기). 이렇게 생성된 정규식이 리터럴만 포함하면, 위의 구조들은 (쌍별로) 동등한 대안처럼 동일하게 동작해요. 하지만 생성된 정규식에 메타문자가 들어가면 동작이 달라져서 혼란스럽게 놀랄 수 있어요.

쉽게 혼동할 수 있는 처음 두 구조는 $variable<$variable>이에요.

my $variable = 'camelia';
say ‘I ♥ camelia’ ~~ /  $variable  /;   # OUTPUT: 「camelia」
say ‘I ♥ camelia’ ~~ / <$variable> /;   # OUTPUT: 「camelia」

여기서는 $variable의 값이 리터럴로 이루어졌으므로 똑같이 동작해요. 하지만 변수가 정규식 메타문자를 포함하도록 바뀌면 출력이 달라져요.

my $variable = '#camelia';
say ‘I ♥ #camelia’ ~~ /  $variable  /;   # OUTPUT: «「#camelia」␤»
say ‘I ♥ #camelia’ ~~ / <$variable> /;   # !! Error: malformed regex

여기서 일어나는 일은 문자열 #camelia가 메타문자 #을 포함한다는 것이에요. 정규식 맥락에서 이 문자는 문자 그대로 일치시키려면 인용되어야 해요. 인용하지 않으면 #은 줄 끝까지 가는 주석의 시작으로 파싱되고, 이는 결국 정규식이 종료되지 않게 해서 malformed가 돼요.

마찬가지로 서로 구분해야 하는 다른 두 구조는 $(code)<{code}>이에요. 이전과 마찬가지로 code의 (문자열화된) 반환 값이 리터럴만으로 이루어지는 한 둘 사이에 차이가 없어요.

my $variable = 'ailemac';
say ‘I ♥ camelia’ ~~ / $($variable.flip)   /;   # OUTPUT: «「camelia」␤»
say ‘I ♥ camelia’ ~~ / <{$variable.flip}>  /;   # OUTPUT: «「camelia」␤»

하지만 반환 값이 정규식 메타문자를 포함하도록 바뀌면 출력이 갈라져요.

my $variable = 'ailema.';
say ‘I ♥ camelia’ ~~ / $($variable.flip)   /;   # OUTPUT: Nil
say ‘I ♥ camelia’ ~~ / <{$variable.flip}>  /;   # OUTPUT: «「camelia」␤»

이 경우 코드의 반환 값은 문자열 .amelia인데, 메타문자 .을 포함해요. 점을 문자 그대로 일치시키려는 $(code)의 시도는 실패하고, 점을 정규식 와일드카드로 일치시키려는 <{code}>의 시도는 성공해요. 그래서 출력이 다르죠.

| vs ||: 어느 분기가 이길까

여러 가능한 대안 중 하나를 일치시키려면 || 또는 |을 쓸 거예요. 그런데 둘은 아주 달라요. 일치하는 분기가 여러 개 있을 때, ||로 구분된 것들은 첫 번째 일치하는 분기가 이기고, |로 구분된 것들은 LTM 전략이 무엇이 이길지 결정해요. || 문서| 문서도 참고하세요.

단순한 정규식에서는 | 대신 ||을 쓰면 익숙한 의미론을 얻을 수 있어요. 하지만 grammar를 작성한다면 LTM과 선언적 접두사에 대해 배우고 |을 선호하는 것이 유용해요. 그리고 같은 정규식에서 둘을 함께 쓰는 것은 피하세요. 꼭 그렇게 해야 한다면 괄호를 추가하고 LTM 전략이 어떻게 동작하는지 알고 코드가 원하는 대로 동작하게 하세요.

이 함정은 보통 같은 정규식에서 |||을 모두 섞으려 할 때 생겨요.

say 42 ~~ / [  0 || 42 ] | 4/; # OUTPUT: «「4」␤»
say 42 ~~ / [ 42 ||  0 ] | 4/; # OUTPUT: «「42」␤»

위 코드는 틀린 결과를 만드는 것처럼 보일 수 있지만, 구현은 실제로 올바르게 동작해요.

$/은 정규식이 일치될 때마다 바뀐다

정규식이 무언가에 대해 일치될 때마다, 결과 Match 객체를 담는 특수 변수 $/이 그 매치의 결과(그것이 Nil일 수도 있음)에 따라 바뀌어요. $/은 정규식이 일치되는 스코프와는 무관하게 바뀌어요.

더 많은 정보와 예시는 정규식 문서의 관련 섹션을 참고하세요.

<foo> vs < foo>: 명명된 규칙 vs 인용된 목록

정규식은 인용된 목록을 포함할 수 있어요. 목록의 요소에 | 분기가 지정된 것처럼 최장 토큰 매칭이 수행되죠(자세한 내용은 여기). 정규식 안에서 다음은 단일 항목 'foo'를 가진 목록이에요.

say 'foo' ~~ /< foo >/;  # OUTPUT: «「foo」␤»
say 'foo' ~~ /< foo>/;   # OUTPUT: «「foo」␤»

하지만 이것은 명명된 규칙 foo에 대한 호출이에요.

say 'foo' ~~ /<foo>/;
# OUTPUT: «No such method 'foo' for invocant of type 'Match'␤ in block <unit> at <unknown file> line 1␤»

그 차이를 조심하세요. 인용된 목록을 사용하려는 것이라면 초기 < 뒤에 공백이 오도록 하세요.

목록 컨텍스트의 비캡처, 비전역 매칭

Perl과 달리, 목록 컨텍스트에서 비캡처·비전역 매칭은 어떤 값도 만들어 내지 않아요.

if  'x' ~~ /./ { say 'yes' }  # OUTPUT: «yes␤»
for 'x' ~~ /./ { say 'yes' }  # NO OUTPUT

그 이유는 (Capture 클래스에서 상속된) 'list' 슬롯이 원본 Match 객체로 채워지지 않기 때문이에요.

say ('x' ~~ /./).list  # OUTPUT: «()␤»

원하는 결과를 얻으려면 전역 매칭, 캡처 괄호, 또는 끝에 쉼표가 있는 목록을 사용하세요.

for 'x' ~~ m:g/./ { say 'yes' }  # OUTPUT: «yes␤»
for 'x' ~~ /(.)/  { say 'yes' }  # OUTPUT: «yes␤»
for ('x' ~~ /./,) { say 'yes' }  # OUTPUT: «yes␤»

흔한 우선순위 실수

부사와 우선순위

부사에도 우선순위가 있어서 화면에 보이는 연산자 순서를 따르지 않을 수 있어요. 같은 우선순위의 두 연산자 뒤에 부사가 오면, 추상 구문 트리에서 처음 찾은 연산자를 선택해요. Raku가 여러분의 뜻을 이해하게 하려면 괄호를 쓰거나 더 느슨한 우선순위의 연산자를 사용하세요.

my %x = a => 42;
say !%x<b>:exists;            # dies with X::AdHoc
say %x<b>:!exists;            # this works
say !(%x<b>:exists);          # works too
say not %x<b>:exists;         # works as well
say True unless %x<b>:exists; # avoid negation altogether

Range와 우선순위

..의 느슨한 우선순위는 몇몇 오류를 일으킬 수 있어요. 전체 범위에 대해 연산하려면 보통 범위를 괄호로 묶는 것이 좋아요.

1..3.say;    # OUTPUT: «3␤» (and warns about useless use of "..")
(1..3).say;  # OUTPUT: «1..3␤»

느슨한 Boolean 연산자

and, or 등의 우선순위는 루틴 호출보다 느슨해요. return, last처럼 다른 언어에서 연산자나 문장이 될 루틴에 대한 호출에서 놀라운 결과를 만들 수 있어요.

sub f {
    return True and False;
    # this is actually
    # (return True) and False;
}
say f; # OUTPUT: «True␤»

지수 연산자와 접두 마이너스

say -1²;   # OUTPUT: «-1␤»
say -1**2; # OUTPUT: «-1␤»

보통 수학 계산에서는 거듭제곱이 마이너스보다 우선해요. 그래서 -1²-(1²)로 쓸 수 있어요. Raku는 이 수학 규칙을 따르고 ** 연산자의 우선순위는 접두 -보다 타이트해요. 음수를 거듭제곱하려면 괄호를 사용하세요.

say (-1)²;   # OUTPUT: «1␤»
say (-1)**2; # OUTPUT: «1␤»

메서드 연산자 호출과 접두 마이너스

접두 마이너스는 점으로 된 메서드 연산 호출보다 더 느슨하게 바인딩돼요. 접두 마이너스는 메서드의 반환 값에 적용돼요. 마이너스가 인자의 일부로 전달되게 하려면 괄호로 감싸세요.

say  -1.abs;  # OUTPUT: «-1␤»
say (-1).abs; # OUTPUT: «1␤»

서브루틴과 메서드 호출

서브루틴과 메서드 호출은 두 가지 형태 중 하나로 할 수 있어요.

foo(...); # function call form, where ... represent the required arguments
foo ...;  # list op form, where ... represent the required arguments

함수 호출 형태는 함수 또는 메서드 이름 뒤와 여는 괄호 앞에 공백이 추가되면 부주의한 사람에게 문제를 일으킬 수 있어요. 먼저 인자가 0개나 1개인 함수를 생각해 보세요.

sub foo() { say 'no arg' }
sub bar($a) { say "one arg: $a" }

이름 뒤에 공백이 있거나 없는 각각을 실행해 보세요.

foo();    # okay: no arg
foo ();   # FAIL: Too many positionals passed; expected 0 arguments but got 1
bar($a);  # okay: one arg: 1
bar ($a); # okay: one arg: 1

이제 인자가 두 개인 함수를 선언해 보세요.

sub foo($a, $b) { say "two args: $a, $b" }

이름 뒤에 공백이 있거나 없이 실행해 보세요.

foo($a, $b);  # okay: two args: 1, 2
foo ($a, $b); # FAIL: Too few positionals passed; expected 2 arguments but got 1

교훈은 이래요. "함수 호출 형식을 쓸 때 sub와 메서드 이름 뒤의 공백에 조심하세요." 일반적인 규칙으로, 함수 호출 형식을 쓸 때 함수 이름 뒤 공백을 피하는 것이 좋은 습관이에요.

함수 호출 형식과 공백으로 오류를 제거하는 영리한 방법들이 있지만, 그것은 해킹에 가까워서 여기서 언급하지 않아요. 더 많은 정보는 Functions를 참고하세요.

마지막으로, 현재 함수를 선언할 때는 함수나 메서드 이름과 파라미터 목록을 둘러싼 괄호 사이에 공백을 써도 문제없다는 점을 주의하세요.

명명된 파라미터

많은 내장 서브루틴과 메서드 호출이 명명된 파라미터를 받고, 여러분의 코드도 받을 수 있어요. 하지만 루틴을 호출할 때 전달하는 인자가 실제로 명명된 파라미터인지 확인하세요.

sub foo($a, :$b) { ... }
foo(1, 'b' => 2); # FAIL: Too many positionals passed; expected 1 argument but got 2

무슨 일이 일어났죠? 두 번째 인자는 명명된 파라미터 인자가 아니라, 위치 인자로 전달된 Pair이에요. 명명된 파라미터를 원하면 그것이 이름처럼 보여야 해요.

foo(1, b => 2); # okay
foo(1, :b(2));  # okay
foo(1, :b<it>); # okay

my $b = 2;
foo(1, :b($b)); # okay, but redundant
foo(1, :$b);    # okay

# Or even...
my %arg = 'b' => 2;
foo(1, |%arg);  # okay too

마지막 것은 혼란스러울 수 있어요. 하지만 Hash| 접두사를 쓰면, 그것은 변수의 내용을 인자로 쓰고 싶다는 것을 나타내는 특수 컴파일러 구성이에요. 해시의 경우 그 내용을 명명된 인자로 취급하라는 뜻이죠.

정말 페어로 전달하고 싶다면 대신 ListCapture를 사용하세요.

my $list = ('b' => 2),; # this is a List containing a single Pair
foo(|$list, :$b);       # okay: we passed the pair 'b' => 2 to the first argument
foo(1, |$list);         # FAIL: Too many positionals passed; expected 1 argument but got 2
foo(1, |$list.Capture); # OK: .Capture call converts all Pair objects to named args in a Capture
my $cap = \('b' => 2); # a Capture with a single positional value
foo(|$cap, :$b); # okay: we passed the pair 'b' => 2 to the first argument
foo(1, |$cap);   # FAIL: Too many positionals passed; expected 1 argument but got 2

Capture는 보통 이에 가장 좋은 선택이에요. 일반 호출 중 루틴 인자의 보통 캡처와 정확히 똑같이 동작하거든요. 여기서 구분의 장점은 개발자가 페어를 명명된 인자나 위치 인자로 전달하는 선택지를 갖게 해 준다는 것이에요. 여러 경우에 유용할 수 있어요.

인자 개수 제한

보통은 눈에 띄지 않지만, 백엔드에 따라 달라지는 인자 개수 제한이 있어요. 임의 크기의 배열을 인자로 평탄화하는 어떤 코드든 너무 많은 요소가 있으면 동작하지 않아요.

my @a = 1 xx 9999;
my @b;
@b.push: |@a;
say @b.elems # OUTPUT: «9999␤»
my @a = 1 xx 999999;
my @b;
@b.push: |@a; # OUTPUT: «Too many arguments in flattening array.␤  in block <unit> at <tmp> line 1␤␤»

평탄화가 없도록 코드를 다시 작성해 이 함정을 피하세요. 위 예에서 pushappend로 바꿀 수 있어요. 이러면 배열을 그대로 전달할 수 있어 평탄화가 필요 없어요.

my @a = 1 xx 999999;
my @b;
@b.append: @a;
say @b.elems # OUTPUT: «999999␤»

Phaser와 암시적 반환

sub returns-ret () {
    CATCH {
        default {}
    }
    "ret";
}

sub doesn't-return-ret () {
    "ret";
    CATCH {
        default {}
    }
}

say returns-ret;        # OUTPUT: «ret␤»
say doesn't-return-ret;
# BAD: outputs «Nil» and a warning «Useless use of constant string "ret" in sink context (line 13)»

returns-retdoesn't-return-ret의 코드는 정확히 똑같아 보일 수 있어요. 원칙적으로 CATCH 블록이 어디에 가는지는 중요하지 않으니까요. 하지만 블록은 객체이고, sub의 마지막 객체가 반환되므로, doesn't-return-retNil을 반환하고, 게다가 "ret"이 이제 싱크 컨텍스트에 있으므로 경고를 발행해요. 관례적인 이유로 phaser를 마지막에 두고 싶다면 return의 명시적 형태를 사용하세요.

sub explicitly-return-ret () {
    return "ret";
    CATCH {
        default {}
    }
}

LEAVE는 sub에서 명시적 반환을 해야 실행된다

LEAVE phaser 문서가 나타내듯, LEAVE는 블록이 종료될 때 실행돼요. "... 프로그램이 갑자기 종료되는 경우는 제외하고"요. 다시 말해 END와 달리, LEAVE는 블록이 질서 있게 반환할 때만 호출돼요. 그래서:

sub a() { LEAVE say "left"; exit 1 }; # No output, it will simply exit

는 기술적으로 반환하는 것이 아니므로 LEAVE 코드를 실행하지 않아요. 반면 END는 프로그램 실행 phaser라서 무슨 일이 있어도 실행돼요.

sub a() {
    END say "Begone"; exit 1
}; a; # OUTPUT: «Begone␤»

입력과 출력

열린 파일핸들과 파이프 닫기

다른 언어와 달리 Raku는 참조 카운팅을 사용하지 않으므로, 파일핸들은 스코프를 벗어나도 닫히지 않아요. close 루틴을 쓰거나, 여러 IO::Handle's 메서드가 받는 :close 인자를 써서 명시적으로 닫아야 해요. 자세한 내용은 IO::Handle.close를 참고하세요.

같은 규칙이 IO::Handle's 하위 클래스인 IO::Pipe에도 적용돼요. runshell 루틴으로 얻는 Proc을 읽을 때 다루는 대상이 바로 그것이죠. 이 주의 사항은 IO::CatHandle 타입에도 적용되는데, 덜 심각해요. 자세한 내용은 IO::CatHandle.close를 참고하세요.

IO::Path 문자열화

부분적으로는 역사적 이유로, 부분적으로는 설계로, IO::Path 객체는 그 CWD 속성을 고려하지 않고 문자열화돼요. 이 말은 chdir한 뒤 IO::Path를 문자열화하거나, 커스텀 $!CWD 속성으로 IO::Path를 문자열화하면, 결과 문자열이 원본 파일시스템 객체를 가리키지 않을 수 있다는 뜻이에요.

with 'foo'.IO {
    .Str.say;       # OUTPUT: «foo␤»
    .relative.say;  # OUTPUT: «foo␤»

    chdir "/tmp";
    .Str.say;       # OUTPUT: «foo␤»
    .relative.say   # OUTPUT: «../home/camelia/foo␤»
}

# Deletes ./foo, not /bar/foo
unlink IO::Path.new("foo", :CWD</bar>).Str

이 문제를 피하는 쉬운 방법은 IO::Path 객체를 전혀 문자열화하지 않는 것이에요. 경로로 작업하는 핵심 루틴은 IO::Path 객체를 받을 수 있으므로 경로를 문자열화할 필요가 없어요. IO::Path의 문자열화 버전이 필요한 경우가 있다면, absoluterelative 메서드를 써서 각각 절대 경로, 상대 경로로 문자열화하세요.

코드에서 chdir을 써서 이 문제를 겪고 있다면, 현재 디렉터리를 바꾸지 않는 방식으로 다시 작성하는 것을 고려하세요. 예를 들어 chdir을 쓰지 않고 runcwd 명명 인자를 전달할 수 있어요.

입력 데이터를 줄로 나누기

IO::Handle.lines를 쓰는 것과 Str에 쓰는 것 사이에는 차이가 있어요. 둘 다 데이터를 같은 방식으로 나눈다고 가정하기 시작하면 함정이 생겨요.

say $_.raku for $*IN.lines # .lines called on IO::Handle
# OUTPUT:
# "foox"
# "fooy\rbar"
# "fooz"

위 예에서 볼 수 있듯 \r("캐리지 리턴" 제어 문자)를 포함한 줄이 있었어요. 하지만 입력은 엄격히 \n으로 나뉘므로, \r은 문자열의 일부로 유지됐어요. 반면 Str.lines은 서로 다른 운영체제의 데이터를 처리하는 데 "똑똑하게" 동작하려고 해요. 그래서 새 줄의 가능한 모든 변형으로 나눠요.

say $_.raku for $*IN.slurp(:bin).decode.lines # .lines called on a Str
# OUTPUT:
# "foox"
# "fooy"
# "bar"
# "fooz"

규칙은 꽤 단순해요. 프로그램이 생성한 출력을 다룰 때는 IO::Handle.lines를, 사용자가 쓴 텍스트를 다룰 때는 Str.lines을 쓰세요. IO::Handle.lines의 동작이 필요한데 원본 IO::Handle이 없으면 $data.split("\n")을 쓰세요.

정말 데이터를 먼저 slurp하고 싶다면 .IO.slurp(:bin).decode.split("\n")을 써야 한다는 점에 주의하세요. :bin을 써서 디코딩을 막고, 나중에 어차피 .decode를 호출한다는 걸 눈여겨보세요. 이 모든 것이 필요한 이유는 .slurp가 텍스트로 작업한다고 가정해서 새 줄에 대해 똑똑하게 동작하려 하기 때문이에요.

Proc::Async를 쓰고 있다면 현재 데이터를 올바르게 나누게 할 쉬운 방법이 없어요. 전체 출력을 읽고 Str.split을 쓰거나(대량 데이터를 다루면 불가능), 들어오는 데이터를 필요한 방식으로 나누는 자신만의 로직을 작성할 수 있어요. 데이터가 null로 구분되는 경우에도 마찬가지예요.

Proc::Asyncprint

Proc::Async를 쓸 때 .print(또는 다른 비슷한 메서드)이 동기적이라고 가정하면 안 돼요. 이 함정의 가장 큰 문제는 코드를 한 번 실행해서는 문제를 알아차리지 못할 가능성이 높아, 발견하기 어려운 간헐적 실패를 일으킬 수 있다는 것이에요.

문제를 보여주는 예시를 볼게요.

loop {
    my $proc = Proc::Async.new: :w, ‘head’, ‘-n’, ‘1’;
    my $got-something;
    react {
        whenever $proc.stdout.lines { $got-something = True }
        whenever $proc.start        { die ‘FAIL!’ unless $got-something }

        $proc.print: “one\ntwo\nthree\nfour”;
        $proc.close-stdin;
    }
    say $++;
}

그리고 그것이 만들어 낼 수 있는 출력:

0
1
2
3
An operation first awaited:
  in block <unit> at print.raku line 4

Died with the exception:
    FAIL!
      in block  at print.raku line 6

.print가 기다릴 수 있는 promise를 돌려주기 때문에 이것을 해결하는 것은 쉬워요. react 블록에서 작업한다면 해결책은 더 아름다워요.

whenever $proc.print: “one\ntwo\nthree\nfour” {
    $proc.close-stdin;
}

.lines 없이 .stdout 사용하기

Proc::Async.stdout 메서드는 줄이 아니라 데이터 _덩어리(chunks)_를 방출하는 supply를 돌려줘요. 함정은 사람들이 그것이 바로 줄을 줄 것이라고 가정하는 경우가 있다는 것이에요.

my $proc = Proc::Async.new(‘cat’, ‘/usr/share/dict/words’);
react {
    whenever $proc.stdout.head(1) { .say } # ← WRONG (most likely)
    whenever $proc.start { }
}

출력은 분명히 줄 1개가 아니에요.

A
A's
AMD
AMD's
AOL
AOL's
Aachen
Aachen's
Aaliyah
Aaliyah's
Aaron
Aaron's
Abbas
Abbas's
Abbasid
Abbasid's
Abbott
Abbott's
Abby
Abby's

줄로 작업하고 싶다면 $proc.stdout.lines을 사용하세요. 전체 출력이 목적이라면 whenever $proc.stdout { $out ~= $_ } 같은 게 효과가 있을 거예요.

예외 처리

Sunk된 Proc

일부 메서드는 Proc 객체를 돌려줘요. 그것이 실패한 프로세스를 나타낸다면, Proc 자체는 예외처럼 동작하지 않지만, 그것을 sinking하면 X::Proc::Unsuccessful 예외가 던져져요. 이는 try가 있어도 이 구성이 예외를 던진다는 뜻이에요.

try run("raku", "-e", "exit 42");
say "still alive";
# OUTPUT: «The spawned process exited unsuccessfully (exit code: 42)␤»

이유는 tryProc을 받아 돌려주고, 그 지점에서 sink되어 예외를 던지기 때문이에요. try 안에서 명시적으로 sinking하면 이 문제를 피하고 예외가 try 안에서 던져지게 보장해요.

try sink run("raku", "-e", "exit 42");
say "still alive";
# OUTPUT: «still alive␤»

어떤 예외도 잡는 데 관심이 없다면 익명 변수를 써서 반환된 Proc을 담아 두세요. 이러면 절대 sink되지 않아요.

$ = run("raku", "-e", "exit 42");
say "still alive";
# OUTPUT: «still alive␤»

단축키 사용하기

^ 트위길

^ 트위길을 쓰면 작은 코드 블록을 작성할 때 상당한 시간과 공간을 아낄 수 있어요. 예:

for 1..8 -> $a, $b { say $a + $b; }

이것은 그냥 이렇게 줄일 수 있어요.

for 1..8 { say $^a + $^b; }

문제는 사람이 변수에 한 글자 대신 더 복잡한 이름을 쓰고 싶을 때 생겨요. ^ 트위길은 위치 변수가 순서가 뒤바뀌고 원하는 대로 이름 붙을 수 있지만, 변수의 Unicode 순서에 따라 값을 할당해요. 위 예에서 $^a$^b가 자리를 바꿔도 그 변수들은 위치 값을 유지해요. Unicode 문자 'a'가 문자 'b'보다 앞에 오기 때문이죠. 예:

# In order
sub f1 { say "$^first $^second"; }
f1 "Hello", "there";    # OUTPUT: «Hello there␤»
# Out of order
sub f2 { say "$^second $^first"; }
f2 "Hello", "there";    # OUTPUT: «there Hello␤»

변수가 아무 이름이나 될 수 있기 때문에, Raku가 이 변수들을 처리하는 방식에 익숙하지 않으면 문제가 생길 수 있어요.

# BAD NAMING: alphabetically `four` comes first and gets value `1` in it:
for 1..4 { say "$^one $^two $^three $^four"; }    # OUTPUT: «2 4 3 1␤»

# GOOD NAMING: variables' naming makes it clear how they sort alphabetically:
for 1..4 { say "$^a $^b $^c $^d"; }               # OUTPUT: «1 2 3 4␤»

»map을 서로 바꿔 쓰기

»map을 쓰는 더 짧은 방법처럼 보일 수 있지만, 몇 가지 핵심 측면에서 다르다.

첫째, »은 컴파일러에게 실행을 자동 스레드화(autothread)할 수 있다는 _힌트_를 포함해요. 그래서 사이드 이펙트를 만드는 루틴을 호출하는 데 사용하면, 그 사이드 이펙트가 순서 없이 발생할 수 있어요(연산자의 결과 순서대로 유지되지만). 또한 호출되는 루틴이 리소스에 접근하면 여러 호출이 서로 다른 스레드에서 동시에 발생할 수 있어 경쟁 조건의 가능성이 있어요.

<a b c d>».say # OUTPUT: «d␤b␤c␤a␤»

둘째, »은 호출되는 루틴의 nodality를 검사하고, 그에 따라 목록을 맵할 때 deepmap이나 nodemap을 사용해요. 이는 map 호출이 맵하는 방식과 다를 수 있어요.

say ((1, 2, 3), [^4], '5')».Numeric;       # OUTPUT: «((1 2 3) [0 1 2 3] 5)␤»
say ((1, 2, 3), [^4], '5').map: *.Numeric; # OUTPUT: «(3 4 5)␤»

결론은 map»은 상호 교환할 수 없지만, 차이를 이해한다면 하나 대신 다른 것을 써도 괜찮다는 것이에요.

« »에서의 단어 나누기

« »이 셸이 하는 것처럼 단어 나누기를 수행한다는 점을 기억하세요. 그래서 많은 셸 함정이 여기에도 적용돼요(특히 run과 함께 쓸 때).

my $file = ‘--my arbitrary filename’;
run ‘touch’, ‘--’, $file;  # RIGHT
run <touch -->, $file;     # RIGHT

run «touch -- "$file"»;    # RIGHT but WRONG if you forget quotes
run «touch -- $file»;      # WRONG; touches ‘--my’, ‘arbitrary’ and ‘filename’
run ‘touch’, $file;        # WRONG; error from `touch`
run «touch "$file"»;       # WRONG; error from `touch`

많은 프로그램이 명령줄 인자와 하이픈으로 시작하는 파일 이름을 구분하려면 --을 요구한다는 점을 주의하세요.

스코프

once 블록 사용하기

once 블록은 부모 블록이 실행될 때 단 한 번만 실행되는 코드 블록이에요. 예:

my $var = 0;
for 1..10 {
    once { $var++; }
}
say "Variable = $var";    # OUTPUT: «Variable = 1␤»

이 기능은 for 루프뿐 아니라 subwhile 같은 다른 코드 블록에도 적용돼요. 하지만 once 블록을 다른 코드 블록 안에 중첩하려고 하면 문제가 생겨요.

my $var = 0;
for 1..10 {
    do { once { $var++; } }
}
say "Variable = $var";    # OUTPUT: «Variable = 10␤»

위 예에서 once 블록은 for 루프 코드 블록 안에 있는 코드 블록 안에 중첩됐어요. 이는 once 블록이 여러 번 실행되게 해요. once 블록은 상태 변수로 이전에 실행됐는지 결정하기 때문이에요. 이 말은 부모 코드 블록이 스코프를 벗어나면, once 블록이 이전에 실행됐는지 추적하는 데 쓰는 상태 변수도 스코프를 벗어난다는 뜻이에요. 그래서 once 블록과 state 변수가 코드 블록 하나 이상 속에 묻히면 원치 않는 동작을 일으킬 수 있어요.

once 블록의 기능을 모방하되 코드 블록 몇 개 깊이 묻혀도 동작하는 것을 원한다면, once 블록의 기능을 수동으로 만들 수 있어요. 위 예를 사용해 state 변수의 스코프를 바꿔 do 블록 안에서도 단 한 번만 실행되게 바꿀 수 있어요.

my $var = 0;
for 1..10 {
    state $run-code = True;
    do { if ($run-code) { $run-code = False; $var++; } }
}
say "Variable = $var";    # OUTPUT: «Variable = 1␤»

이 예시에서 우리는 본질적으로 once 블록을 수동으로 만든 거예요. 여러 번 실행될 최상위 수준에 $run-code라는 state 변수를 만들고, 일반 if$run-codeTrue인지 검사해요. $run-codeTrue이면 변수를 False로 만들고 한 번만 완료되어야 하는 코드를 계속 진행하죠.

위 예시처럼 state 변수를 쓰는 것과 일반 once 블록을 쓰는 것의 주요 차이는 state 변수가 어느 스코프에 있느냐예요. once 블록이 만든 state 변수의 스코프는 블록을 둔 곳과 같아요('once'라는 단어를 상태 변수와 그 변수를 보는 if로 바꾼다고 상상해 보세요). 위 state 변수 예시가 동작하는 이유는 변수가 반복될 최상위 스코프에 있기 때문이에요. 반면 do 안에 once 블록이 있는 예시는 변수를 do 블록 안에 만들어, 반복되는 최상위 스코프가 아니게 한 거예요.

클래스 메서드 안에서 once 블록을 쓰면 그 once 상태가 그 클래스의 모든 인스턴스에 걸쳐 이어져요. 예:

class A {
    method sayit() { once say 'hi' }
}
my $a = A.new;
$a.sayit;      # OUTPUT: «hi␤»
my $b = A.new;
$b.sayit;      # nothing

LEAVE phaser와 exit

정상적인 리소스 종료를 수행하는 데 LEAVE phaser를 쓰는 것은 흔한 패턴이지만, 프로그램이 exit로 멈추는 경우는 다루지 못해요. 다음 비결정적 예시가 이 함정의 복잡성을 보여줘야 해요.

my $x = say ‘Opened some resource’;
LEAVE say ‘Closing the resource gracefully’ with $x;

exit 42 if rand < ⅓; # ① 「exit」 is bad
die ‘Dying because of unhandled exception’ if rand < ½; # ② 「die」 is ok
# fallthru ③

세 가지 가능한 결과가 있어요.

①
Opened some resource

②
Opened some resource
Closing the resource gracefully
Dying because of unhandled exception
  in block <unit> at print.raku line 5

③
Opened some resource
Closing the resource gracefully

exit 호출은 많은 프로그램에서 정상 동작의 일부예요. 그래서 LEAVE phaser와 exit 호출의 의도하지 않은 결합을 조심하세요.

LEAVE phaser는 생각보다 일찍 실행될 수 있다

파라미터 바인딩은 루틴의 블록 "안에" 있을 때 실행돼요. 이는 잘못된 인자가 주어졌을 때 파라미터 바인딩이 실패하면, 그 블록을 떠날 때 LEAVE phaser가 실행된다는 뜻이에요.

sub foo(Int) {
    my $x = 42;
    LEAVE say $x.Int; # ← WRONG; assumes that $x is set
}
say foo rand; # OUTPUT: «No such method 'Int' for invocant of type 'Any'␤»

이 문제를 피하는 간단한 방법은 sub나 method를 multi로 선언하는 것이에요. 그러면 디스패치 중에 후보가 제거되고 코드는 서브 안의 어떤 것도 바인딩하지 않게 되어 루틴 본문에 들어가지 않아요.

multi foo(Int) {
    my $x = 42;
    LEAVE say $x.Int;
}
say foo rand; # OUTPUT: «Cannot resolve caller foo(Num); none of these signatures match: (Int)␤»

또 다른 대안은 LEAVE를 다른 블록 안에 두는 것이에요( 블록을 떠날 때 실행되는 것이 적절하다고 가정한다면, 루틴 본문이 아니라요).

sub foo(Int) {
    my $x = 42;
    { LEAVE say $x.Int; }
}
say foo rand; # OUTPUT: «Type check failed in binding to parameter '<anon>'; expected Int but got Num (0.7289418947969465e0)␤»

실패한 인자 바인딩으로 루틴을 떠나더라도 LEAVE가 실행되도록 보장할 수도 있어요. 예시에서 $x를 뭔가 하기 전에 is defined로 검사해요.

sub foo(Int) {
    my $x = 42;
    LEAVE $x andthen .Int.say;
}
say foo rand; # OUTPUT: «Type check failed in binding to parameter '<anon>'; expected Int but got Num (0.8517160389079508e0)␤»

Grammar

grammar의 액션 안에서 정규식 사용하기

# Define a grammar
grammar will-fail {
    token TOP {^ <word> $}
    token word { \w+ }
}

# Define an action class
class will-fail-actions {
    method TOP ($/) {      # <- note the $/ in the signature, which is readonly
        my $foo = ~$/;
        say $foo ~~ /foo/; # <- the regex tries to assign the result to $/ and will fail
    }
}

# Try to parse something...
will-fail.parse('word', :actions(will-fail-actions));
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot assign to a readonly variable or a value␤»

메서드 TOP에서 Cannot assign to a readonly variable ($/) or a value로 실패해요. 여기 문제는 정규식도 $/에 영향을 준다는 것이에요. $/TOP의 시그니처에 있어 읽기 전용 변수이므로 오류가 나는 거예요. 시그니처에서 다른 변수를 쓰거나 is copy를 추가하면 안전해요.

method TOP ($/ is copy) { my $foo = ~$/; my $v = $foo ~~ /foo/;  }

규칙/token/정규식에 특정 이름 사용하기

Grammar는 사실 클래스의 한 종류예요.

grammar G {};
say G.^mro; # OUTPUT: «((G) (Grammar) (Match) (Capture) (Cool) (Any) (Mu))␤»

^mro는 이 빈 grammar의 클래스 계층을 출력하며 모든 슈퍼클래스를 보여줘요. 그리고 이 슈퍼클래스들은 저마다의 메서드를 가지고 있어요. 그 grammar에서 메서드를 정의하면 클래스 계층에 있는 것들과 충돌할 수 있어요.

grammar g {
    token TOP { <item> };
    token item { 'defined' }
};
say g.parse('defined');
# OUTPUT: «Too many positionals passed; expected 1 argument but got 2␤  in regex item at /tmp/grammar-clash.raku line 3␤  in regex TOP at /tmp/grammar-clash.raku line 2␤  in block <unit> at /tmp/grammar-clash.raku line 5»

item은 무해해 보이지만, Mu 클래스에 정의된 sub이에요. 메시지는 좀 난해하고 그 사실과 전혀 관련 없지만, 그래서 이것이 함정으로 나열된 거예요. 일반적으로 계층의 어떤 부분에 정의된 모든 sub가 문제를 일으킬 거예요. 일부 메서드도 마찬가지예요. 예를 들어 Mu에 정의된 CREATE, take, defined가 있죠. 일반적으로 multi 메서드와 단순 메서드는 문제가 없지만, 규칙 이름으로 쓰는 것은 좋은 관행이 아닐 수 있어요.

또한 규칙/token/정규식 이름에 phaser를 쓰는 것을 피하세요. TWEAK, BUILD, BUILD-ALL은 그렇게 하면 다른 종류의 예외를 던져요. Cannot find method 'match': no method cache and no .^find_method. 역시 실제로 무슨 일이 벌어지는지와 약간만 관련이 있어요.

안타까운 일반화

키가 둘 이상일 때 :exists

키가 둘 이상인 해시에서 :exists를 쓰고 싶다고 해 볼게요.

my %h = a => 1, b => 2;
say ‘a exists’ if %h<a>:exists;   # ← OK; True
say ‘y exists’ if %h<y>:exists;   # ← OK; False
say ‘Huh‽’     if %h<x y>:exists; # ← WRONG; returns a 2-item list

"그들 중 any라도 존재하는가"라는 뜻이었나요, 아니면 all이 존재해야 한다는 뜻이었나요? Junctionanyall을 사용해 명확히 하세요.

my %h = a => 1, b => 2;
say ‘x or y’     if any %h<x y>:exists;   # ← RIGHT (any); False
say ‘a, x or y’  if any %h<a x y>:exists; # ← RIGHT (any); True
say ‘a, x and y’ if all %h<a x y>:exists; # ← RIGHT (all); False
say ‘a and b’    if all %h<a b>:exists;   # ← RIGHT (all); True

(junction 없이) 항상 True인 이유는 요청된 각 조회에 대해 Bool 값이 있는 목록을 반환하기 때문이에요. 비어 있지 않은 목록은 Bool화할 때 항상 True를 주므로, 어떤 키를 주든 검사는 항상 성공해요.

목록의 목록과 […] 메타연산자 사용하기

가끔 누군가는 [Z]로 목록-목록의 전치(transpose)를 만들 수 있다고 생각해요.

my @matrix = <X Y>, <a b>, <1 2>;
my @transpose = [Z] @matrix; # ← WRONG; but so far so good ↙
say @transpose;              # OUTPUT: «[(X a 1) (Y b 2)]␤»

그리고 모든 게 잘 동작하다가, 정확히 한 행(자식 목록)을 가진 입력 @matrix가 들어오면:

my @matrix = <X Y>,;
my @transpose = [Z] @matrix; # ← WRONG; ↙
say @transpose;              # OUTPUT: «[(X Y)]␤» – not the expected transpose [(X) (Y)]

이것은 부분적으로 단일 인자 규칙 때문이고, 이런 일반화가 동작하지 않을 다른 경우도 있어요.

blob 목록 연결에 [~] 사용하기

~ 중위 연산자Str 또는 Blob을 연결하는 데 쓸 수 있어요. 하지만 빈 목록은 항상Str로 축약돼요. 이는 요소가 없는 목록이 있을 때 reduction 메타연산자가 주어진 연산자의 항등원을 돌려주기 때문이에요. ~의 항등원은 목록에 어떤 종류의 요소가 들어갈 수 있는지와 무관하게 빈 문자열이에요.

my Blob @chunks;
say ([~] @chunks).raku; # OUTPUT: «""␤»

결과가 Blob이라고 가정하고 쓰려고 하면 문제가 생길 수 있어요.

my Blob @chunks;
say ([~] @chunks).decode;
# OUTPUT: «No such method 'decode' for invocant of type 'Str'. Did you mean 'encode'?␤…»

이 경우를 다루는 방법은 많아요. [ ] 메타연산자를 아예 피할 수 있어요.

my @chunks;
# …
say Blob.new: |«@chunks; # OUTPUT: «Blob:0x<>␤»

또는 배열을 빈 Blob으로 초기화할 수 있어요.

my @chunks = Blob.new;
# …
say [~] @chunks; # OUTPUT: «Blob:0x<>␤»

또는 || 연산자를 활용해 목록이 비었을 때 빈 Blob을 쓰게 할 수 있어요.

my @chunks;
# …
say [~] @chunks || Blob.new; # OUTPUT: «Blob:0x<>␤»

다른 연산자로 목록을 축약할 때도 비슷한 문제가 생길 수 있다는 점을 참고하세요.

Maps

싱크 컨텍스트에서 Map 중첩에 주의

Map은 표현식을 List의 모든 요소에 적용하고 Seq을 돌려줘요.

say <þor oðin loki>.map: *.codes; # OUTPUT: «(3 4 4)␤»

Map은 종종 루프의 간결한 대체물로 쓰이며, map 코드 블록 안에서 어떤 동작을 수행해요.

<þor oðin loki>.map: *.codes.say; # OUTPUT: «3␤4␤4␤»

Map이 중첩되고 싱크 컨텍스트에 있을 때 문제가 생길 수 있어요.

<foo bar ber>.map: { $^a.comb.map: { $^b.say}}; # OUTPUT: «»

가장 안쪽 map이 결과를 가장 바깥쪽 map까지 거품처럼 끌어올릴 것이라 기대할 수 있지만, 그것은 아무것도 하지 않아요. Map은 Seq을 돌려주고, 싱크 컨텍스트에서는 가장 안쪽 map이 순회하고 만들어진 값을 버리므로 아무것도 내놓지 않아요. 문장 시작에 say를 쓰기만 해도 결과를 싱크 컨텍스트에서 구할 수 있어요.

say <foo bar ber>.map: *.comb.map: *.say ;
# OUTPUT: «f␤o␤o␤b␤a␤r␤b␤e␤r␤((True True True) (True True True) (True True True))␤»

하지만 이는 의도대로 동작하지 않아요. 첫 번째 f␤o␤o␤b␤a␤r␤b␤e␤r␤는 가장 안쪽 say의 결과인데, sayBool(여기서는 True)을 반환하기 때문이에요. 그 True들이 가장 바깥쪽 say로 출력되는데, 글자마다 하나씩이에요. 훨씬 나은 방법은 가장 바깥쪽 시퀀스를 flatten하는 것이에요.

<foo bar ber>.map({ $^a.comb.map: { $^b.say}}).flat
# OUTPUT: «f␤o␤o␤b␤a␤r␤b␤e␤r␤»

물론 결과를 위해 say를 남겨두면 의도된 결과도 얻을 수 있어요. 두 중첩 시퀀스를 void 컨텍스트에서 구원할 테니까요.

say <foo bar ber>.map: { $^þ.comb }; # OUTPUT: « ((f o o) (b a r) (b e r))␤»

스마트매칭

스마트매치 연산자는 오른쪽이 왼쪽을 받아들이는 것으로 단축해요. 이는 혼란을 일으킬 수 있어요.

스마트매치와 WhateverCode

스마트매치의 왼쪽에 WhateverCode를 쓰는 것은 기대대로, 또는 전혀 동작하지 않아요.

my @a = <1 2 3>;
say @a.grep( *.Int ~~ 2 );
# OUTPUT: «Cannot use Bool as Matcher with '.grep'.  Did you mean to
# use $_ inside a block?␤␤␤»

오류 메시지는 별로 이해가 안 돼요. 하지만 ACCEPTS 메서드로 놓고 보면 이해가 돼요. 그 코드는 2.ACCEPTS( *.Int )와 동등한데, *.IntBlock이라서 Numeric으로 강제 변환할 수 없어요.

해결책: 스마트매치의 왼쪽에 WhateverCode를 쓰지 마세요.

my @a = <1 2 3>;
say @a.grep( 2 ~~ *.Int ); # OUTPUT: «(2)␤»

라이브러리

파일시스템 저장소

라이브러리 검색 경로에 큰 디렉터리를 추가하면, 로드하는 모듈이 추가된 경로 중 어디에도 없어도 모듈 로드 시간에 부정적인 영향을 줄 수 있어요. 컴파일러가 요청된 모듈을 로드하기 전에 트리의 모든 관련 파일을 순회하고 체크섬해야 하기 때문이에요. 디렉터리에 META6.json 파일이 없으면요.

검색 경로에 디렉터리를 추가하는 세 가지 흔한 방법이 있어요.

  • 명령줄에서 Raku에 스위치를 제공: $ raku -Idir
  • 환경 변수 설정: $ env RAKULIB=dir raku
  • lib pragma 사용(즉 use lib 'dir')

이 중 어떤 방법으로 특히 크거나 깊게 중첩된 디렉터리를 추가하면 성능 저하가 뚜렷해요. 예를 들어 전형적인 Unix 머신에서 /usr/lib를 검색 경로에 추가하면 모듈을 로드할 때 지각할 수 있는 성능 영향을 줘요.

    $ time raku -e 'use Test'
    real    0m0.511s
    user    0m0.554s
    sys     0m0.118s

    # no penalty, since we don't load anything:
    $ time raku -I/usr/lib -e ''
    real    0m0.247s
    user    0m0.254s
    sys     0m0.066s

    $ time raku -I/usr/lib -e 'use Test'
    real    0m6.344s
    user    0m6.232s
    sys     0m0.356s

    $ time raku -e 'use lib "/usr/lib"; use Test'
    real    0m6.555s
    user    0m6.445s
    sys     0m0.326s

    $ time env RAKULIB=/usr/lib raku -e 'use Test'
    real    0m6.479s
    user    0m6.368s
    sys     0m0.383s

이미 다른 곳에 설치된 모듈(Test는 Rakudo의 일부로 제공되는데도 /usr/lib가 로드 전에 어쨌든 스캔됨)을 요청할 때조차 실행 시간이 늘어난다는 점을 주목하세요. 이 함정을 피하는 가장 좋은 방법은 큰 트리에 있는 모듈을 설치하고 그 디렉터리를 검색 경로에서 아예 빼는 것이에요. 또는 트리에 META6.json 파일을 추가하면 시간이 걸리는 순회를 방지할 수 있어요.