변수
변수 (variables)
Raku의 변수에 대해 다루는 문서예요.
변수는 값이나 컨테이너를 가리키는 상징적인 이름(symbolic name)이에요. 변수 선언이나 값 할당은 즉석에서 컨테이너를 만들 수도 있어요. 변수 이름은 시길(sigil)이라는 특수 문자로 시작할 수도, 시작하지 않을 수도 있고, 선택적으로 twigil이라는 두 번째 특수 문자가 이어지며, 그다음 식별자(identifier)가 와요.
시길 (Sigils)
시길은 네 가지가 있어요. 스칼라 시길 $, 위치 시길 @, 연관 시길 %, 호출 가능 시길 &이에요.
시길은 문법, 타입 시스템, 컨테이너를 잇는 연결고리예요. 변수를 선언할 때 가장 흔한 타입 제약의 단축키 역할을 하고, 문자열 삽입의 표시자 역할도 해요. 위치 시길과 연관 시길은 어떤 메서드로 디스패치할지 알기 위해 필요한 기본 타입 첨자(base type subscript)를 강제하는 타입 제약을 제공해요. 호출 가능 시길은 함수 호출에서도 같은 일을 해요. 후자는 또 호출의 괄호를 어디서 생략할 수 있는지 컴파일러에 알려줘요. 위치·연관 시길은 기본적으로 펼침(flattening)으로 할당을 단순화하기도 해요.
| 시길 | 타입 제약 | 기본 타입 | 할당 | 예 |
|---|---|---|---|---|
| $ | Mu (타입 제약 없음) | Any | 항목(item) | Int, Str, Array, Hash |
| @ | Positional | Array | 목록(list) | List, Array, Range, Buf |
| % | Associative | Hash | 목록 | Hash, Map, Pair |
| & | Callable | Callable | 항목 | Sub, Method, Block, Routine |
예:
my $square = 9 ** 2;
my @array = 1, 2, 3; # 요소가 세 개인 Array 변수
my %hash = London => 'UK', Berlin => 'Germany';
변수가 묶일 타입은 변수 선언에서 is로 정할 수 있어요. FailHash 클래스가 있다고 가정할게요:
class FailHash is Hash {
has Bool $!final = False;
multi method AT-KEY ( ::?CLASS:D: Str:D \key ){
fail X::OutOfRange.new(:what("Hash key"), :got(key),
:range(self.keys)) if $!final && !self.EXISTS-KEY(key);
callsame # 아직 final이 아니므로 Hash의 정상 동작 수행
}
method finalize() {
$!final = True
}
}
그러면 is로 이런 타입의 %h 변수를 정의할 수 있어요:
my %h is FailHash = oranges => "round", bananas => "bendy";
그리고 다음 코드를 실행해 볼게요:
say %h<oranges>;
# OUTPUT: «round»
%h.finalize;
say %h<cherry>;
CATCH { default { put .^name, ': ', .Str } }
# OUTPUT: «X::OutOfRange: Hash key out of range. Is: cherry, should be in (oranges bananas)»
시길 없는 변수에 대한 내용은 시길 없는 변수(sigilless variables)를 참고하세요.
항목 할당과 목록 할당 (Item and list assignment)
변수 할당은 두 종류가 있어요: 항목 할당(item assignment)과 목록 할당(list assignment)이에요.
항목 할당은 오른쪽의 단일 값을 왼쪽의 Scalar 변수에 복사해요. 단순 Scalar 변수가 아닌 곳에 대한 할당은 목록 할당으로 해석돼요. 목록 할당은 할당 연산이 무엇을 수반하는지 선택을 왼쪽 변수에 맡겨요. 예를 들어 Array 변수(@ 시길)는 목록 할당 시 스스로를 비운 다음 오른쪽의 모든 값을 요소로 반복 복사해요.
두 종류의 할당 모두 = 기호를 연산자로 쓰고 둘 다 오른쪽 결합이지만, 연산자 우선순위는 달라요: 항목 할당은 목록 할당보다 높은 우선순위 레벨(항목 할당 레벨)을 가져요 (목록 접두사 레벨보다 높음). 쉼표로 구분된 요소 목록을 할당하는 상황에서는, 이 우선순위들을 특히 그 사이에 놓이는 쉼표 연산자 ,와 대비해 봐야 해요. 목록을 묶어주는 괄호(또는 다른 구성)가 없다면, 항목 할당은 목록 전체가 아니라 지정된 목록의 첫 요소만 할당해요.
할당 표현식에서 왼쪽의 컨텍스트가 =가 항목 할당인지 목록 할당인지 결정해요. 앞서 말했듯 항목 할당은 단순 Scalar 변수로 제한돼요. 따라서 Scalar 컨테이너에 대한 할당(스칼라 컨텍스트)은 항목 할당을 유발하지만, Scalar를 괄호 ( )로 감싸 명시적으로 목록 컨텍스트에 놓으면 예외예요:
my $a;
$a = 1,2,3; # Scalar에 항목 할당
say $a; # OUTPUT: «1» ( '='가 ','보다 우선순위가 높음 )
my $b = 1,2,3; # Scalar에 항목 할당 (앞 예와 동일)
say $b; # OUTPUT: «1»
my $c;
($c) = 4,5,6; # Scalar에 목록 할당; '( )'는 목록 컨텍스트화
say $c; # OUTPUT: «(4,5,6)»
(my $d) = 4,5,6; # Scalar에 목록 할당 (앞 예와 동일)
say $d; # OUTPUT: «(4,5,6)»
List 컨테이너(목록 컨텍스트)에 대한 할당은 항상 목록 할당을 유발해요:
my @e;
@e = 7,8,9; # Array에 목록 할당
say @e; # OUTPUT: «[7,8,9]»
my $f;
($f,) = 7,8,9; # 요소가 하나인 List에 목록 할당
say $f; # OUTPUT: «7»
say ( ($f,) ).VAR.^name; # OUTPUT: «List»
# 주의: 특별한 선언 문법!
my ($g) = 7,8,9; # 요소가 하나인 List에 목록 할당
say $g; # OUTPUT: «7»
say ( ($g) ).VAR.^name # OUTPUT: «List»
위 마지막 두 예시는 오른쪽 목록의 첫 항목을 고르는 단순한 구조분해(destructuring) 할당이에요. 변수 선언 컨텍스트에서 구조분해 할당에 대한 더 자세한 논의는 어휘·패키지 스코프의 변수 목록 선언 섹션을 참고하세요.
연쇄 할당(chained assignment)은 할당 연산자의 우선순위와, 해당하는 경우 오른쪽 결합성을 고려해 해석돼요. 아래 예시에는 할당 연산자 두 개를 포함한 연쇄 할당 문이 하나 있어요. @array에 대한 할당은 Scalar 변수 $num에 대한 항목 할당보다 낮은 우선순위의 목록 할당이에요. 따라서 Scalar 변수 $num에 대한 항목 할당을 포함한 할당 표현식이 먼저 평가돼요. 그 값 42를 반환하고, 이는 목록 할당보다 우선순위가 높은 쉼표 연산자가 만드는 List (42, "str")의 일부가 돼요. 마지막으로 List (42, "str")가 @array에 목록 할당돼요:
my @array;
@array = my $num = 42, "str"; # parsed as @array = ( (my $num = 42), "str )
say @array.raku; # OUTPUT: «[42, "str"]» (an Array)
say $num.raku; # OUTPUT: «42» (a Num)
변형 하나를 더 볼게요:
my ( @foo, $bar );
@foo = ($bar) = 42, "str"; # parsed as @foo = ( $bar = (42, "str") )
say $bar.raku; # OUTPUT: «$(42, "str")» (a List)#
say @foo.raku; # OUTPUT: «[(42, "str"),]» (an Array)
이 경우 목록 컨텍스트화 ( )가 $bar를 목록 컨텍스트에 놓아서 Scalar 변수 $bar에 목록 할당을 유발해요. 즉 연쇄 목록 할당이 두 개 있는데, 둘 다 List (42, "str")를 만드는 쉼표 연산자 ,보다 우선순위가 낮아요. 오른쪽 결합성 때문에 먼저 평가되는 목록 할당 표현식은 $bar에 대한 할당으로, 할당된 값 $(42, "str")(요소 두 개짜리 List를 담은 Scalar)를 반환해요. 이 값이 다시 @array에 목록 할당돼서, List 하나를 요소로 가진 Array가 돼요.
우선순위와 결합성의 자세한 내용은 연산자 문서를 참고하세요.
시길 없는 변수 (Sigilless variables)
\ 접두어를 쓰면 시길이 없는 변수를 만들 수 있어요:
my \degrees = pi / 180;
my \θ = 15 * degrees;
시길 없는 변수에는 연결된 컨테이너가 없다는 점을 주의하세요. 즉 위의 degrees와 θ는 실제로 Num을 직접 나타내요. 확인하려면 정의한 뒤에 값을 할당해 보세요:
θ = 3; # "Cannot modify an immutable Num" 오류로 죽음
시길 없는 변수는 컨텍스트를 강제하지 않아서, 무언가를 있는 그대로 전달하는 데 쓸 수 있어요:
sub logged(&f, |args) {
say('Calling ' ~ &f.name ~ ' with arguments ' ~ args.raku);
my \result = f(|args);
# ^^^^^^^ 여기서 어떤 컨텍스트도 강제하지 않음
say(&f.name ~ ' returned ' ~ result.raku);
return |result;
}
시길 없는 변수는 결합(binding)에도 쓸 수 있어요. 자세한 내용은 Binding 문서를 참고하세요.
트위길 (Twigils)
트위길(twigil)이라는 용어는 시길(sigil)의 말장난으로, 식별자 앞에 두 개의 기호를 쓴다는 뜻이에요. 두 번째 기호는 시길과 식별자 사이에 오는데, 변수의 스코프, 즉 그 변수가 어디서 정의되고 바뀔 수 있는지와 관련돼요.
| 트위길 | 스코프 |
|---|---|
| (없음) | 선언자에만 기반 |
| * | 동적(Dynamic) |
| ? | 컴파일 타임 변수 |
| ! | 애트리뷰트 (클래스 멤버) |
| . | 메서드 (실제 변수 아님) |
| < | 매치 객체로의 인덱스 (실제 변수 아님) |
| ^ | 자동 선언 형식 위치 매개변수 |
| : | 자동 선언 형식 이름 매개변수 |
| = | Pod 변수 |
| ~ | 이 어휘 위치에서 파서가 보는 하위 언어 |
* 트위길
이 트위길은 바깥(outer) 스코프가 아니라 호출자(caller)의 스코프를 통해 조회되는 동적 변수(dynamic variable)에 써요. 아래 예시를 볼게요.[1]
my $lexical = 1;
my $*dynamic1 = 10;
my $*dynamic2 = 100;
sub say-all() {
say "$lexical, $*dynamic1, $*dynamic2";
}
say-all(); # OUTPUT: 1, 10, 100
{
my $lexical = 2;
my $*dynamic1 = 11;
$*dynamic2 = 101;
say-all(); # OUTPUT: 1, 11, 101
}
say-all(); # OUTPUT: 1, 10, 101
첫 번째 &say-all 호출은 예상대로 "1, 10, 100"을 출력해요. 두 번째는 "1, 11, 101"을 출력하는데, $lexical은 호출자 스코프가 아니라 &say-all이 정의된 스코프에서 조회되기 때문이에요. 두 동적 변수는 호출자 스코프에서 조회되므로 값이 11, 101이에요. 세 번째 호출에서는 $*dynamic1이 더 이상 11이 아니지만 $*dynamic2는 여전히 101이에요. 블록에서 새 동적 변수 $*dynamic1을 선언했고, $*dynamic2처럼 기존 변수에 할당하지 않았기 때문이에요.
동적 변수는 다른 변수와 달라서, 선언되지 않은 동적 변수를 참조하는 것이 컴파일 타임 오류가 아니라 런타임 Failure예요. 그래서 동적 변수는 다른 데 쓰기 전에 정의됨(definedness)을 검사하거나 Boolean 컨텍스트에서 쓰기만 하면 선언 없이 사용할 수 있어요:
sub foo() {
$*FOO // 'foo';
}
say foo; # OUTPUT: «foo»
my $*FOO = 'bar';
say foo; # OUTPUT: «bar»
is dynamic 트레이트도 참고하세요.
동적 변수는 my로 선언하면 어휘 스코프를, our로 선언하면 패키지 스코프를 가질 수 있어요. 동적 해석(dynamic resolution)과 our로 도입되는 심볼 테이블을 통한 해석은 서로 직교하는 문제예요.
? 트위길
? 트위길로 컴파일 타임 변수를 다룰 수 있어요. 컴파일러가 알며, 컴파일된 뒤에는 수정할 수 없어요. 대표적인 예시는:
say "$?FILE: $?LINE"; # OUTPUT: "hello.raku: 23"
# "hello.raku"라는 파일의 23번째 줄이라면
이런 특수 변수 목록은 컴파일 타임 변수 문서를 참고하세요.
! 트위길
애트리뷰트는 클래스 인스턴스마다 존재하는 변수예요. 클래스 내부에서 !로 직접 접근할 수 있어요:
my class Point {
has $.x;
has $.y;
method Str() {
"($!x, $!y)"
}
}
애트리뷰트가 $.x, $.y로 선언됐는데도 $!x, $!y로 접근하는 걸 주목하세요. Raku에서는 모든 애트리뷰트가 비공개(private)이고 $!애트리뷰트-이름으로 클래스 내에서 직접 접근할 수 있기 때문이에요. Raku가 접근자 메서드를 자동으로 만들어 줄 수도 있어요. 객체·클래스·애트리뷰트에 대한 자세한 내용은 객체 지향 문서를 참고하세요.
. 트위길
. 트위길은 사실 변수용이 아니에요. 실제로 이런 코드는:
my class Point {
has $.x;
has $.y;
method Str() {
"($.x, $.y)" # 이번에는 ! 대신 .을 쓴 점을 주목
}
}
self에 자동 생성된 메서드 x와 y를 호출하는 거예요. 애트리뷰트를 선언할 때 . 트위길을 썼기 때문에 그런 메서드가 자동 생성된 거죠. 다만 하위 클래스가 그 메서드들을 오버라이드할 수 있다는 점을 주의하세요. 그걸 원하지 않으면 $!x, $!y를 대신 쓰세요.
. 트위길이 메서드 호출을 한다는 사실은 다음과 같은 것도 가능하다는 뜻이에요:
class SaySomething {
method a() { say "a"; }
method b() { $.a; }
}
SaySomething.b; # OUTPUT: «a»
객체·클래스·애트리뷰트·메서드에 대한 자세한 내용은 객체 지향 문서를 참고하세요.
^ 트위길
^ 트위길은 블록이나 서브루틴에 형식 위치 매개변수(formal positional parameter)를 선언해요. 즉 $^variable 형태의 변수는 일종의 자리표시자(placeholder) 변수예요. 이들은 베어 블록(bare block)에서 그 블록의 형식 매개변수를 선언하는 데 쓸 수 있어요. 그래서 다음 코드의 블록은:
my @powers-of-three = 1,3,9…100;
say reduce { $^b - $^a }, 0, |@powers-of-three;
# OUTPUT: «61»
$a와 $b 두 개의 형식 매개변수를 가져요. 코드에서 $^b가 $^a보다 앞에 나와도 $^a가 여전히 그 블록의 첫 형식 매개변수라는 점에 주목하세요. 자리표시자 변수는 유니코드 순서로 정렬되기 때문이에요.
거의 모든 유효한 식별자를 자리표시자 변수로 쓸 수 있지만, 독자가 당황하지 않도록 짧거나 올바른 순서로 쉽게 파악되는 이름을 쓰는 걸 권장해요.
일반 블록과 서브루틴도 자리표시자 변수를 쓸 수 있지만, 명시적 매개변수 목록이 없을 때만 가능해요.
sub say-it { say $^a; } # 유효
sub say-it() { say $^a; } # 무효
{ say $^a; } # 유효
-> $x, $y, $x { say $^a; } # 무효
자리표시자 변수는 타입 제약을 가질 수 없고, 단일 대문자 한 글자짜리 이름도 될 수 없어요(이것은 몇몇 펄 관용구를 잡아내기 위해 금지돼 있어요).
^ 트위길은 어떤 시길과도 결합해 그 시길을 가진 자리표시자 변수를 만들 수 있어요. 시길은 시길 표의 설명대로 정상적인 의미론적 효과를 가져요. 그래서 @^array, %^hash, &^fun은 모두 유효한 자리표시자 변수예요.
: 트위길
: 트위길은 블록이나 서브루틴에 형식 이름 매개변수(formal named parameter)를 선언해요. 이 형태로 선언한 변수도 일종의 자리표시자 변수예요. 따라서 ^ 트위길로 선언한 변수에 적용되는 것들이 여기에도 적용돼요(단, 위치가 아니므로 유니코드 순서로 정렬되지 않는다는 점 빼고요). 예:
say { $:add ?? $^a + $^b !! $^a - $^b }( 4, 5 ) :!add
# OUTPUT: «-1»
자리표시자 변수에 대한 자세한 내용은 ^ 섹션을 참고하세요.
^와 :에 대한 참고
다른 트위길과 달리 ^와 :은 변수를 선언하며, 이후에는 그 트위길 없이도 그 변수를 참조할 수 있어요. 그래서 앞의 예시는 이렇게 쓸 수 있어요:
say { $:add ?? $^a + $^b !! $a - $b }( 4, 5 ) :!add # OUTPUT: «-1»
즉, $^a로 $a를 선언했다면 같은 스코프에서 그 변수를 $^a 또는 $a 어느 쪽으로든 참조할 수 있어요. :도 마찬가지로 $:add를 선언한 뒤에는 그 선언된 변수를 원하면 $add로 참조할 수 있어요.
어떤 경우엔 이게 그저 편의일 뿐이지만, 중첩 블록을 다룰 때는 훨씬 더 중요해질 수 있어요. 예:
{ say $^a; with "inner" { say $^a } }("outer"); # OUTPUT: «outerinner»
{ say $^a; with "inner" { say $a } }("outer"); # OUTPUT: «outerouter»
첫 줄은 형식 위치 매개변수를 두 개 선언하고, 두 번째 줄은 하나만 선언해요(하지만 두 번 참조). 이는 with, for, if처럼 블록을 만든다는 사실을 크게 의식하지 않고 자주 쓰는 구성에서 특히 중요할 수 있어요.
^ 트위길처럼 : 트위길도 어떤 시길과도 결합할 수 있습니다. :을 시길과 함께 쓰면 그 시길을 가진(시길의 의미론을 적용한) 형식 이름 매개변수를 만들어요. 그래서 @:array, %:hash, &:fun 모두 유효하며 각각 지정된 시길의 형식 이름 매개변수를 만들어요.
= 트위길
= 트위길은 Pod 변수에 접근하는 데 써요. 현재 파일의 모든 Pod 블록은 $=data, $=SYNOPSIS, =UserBlock 같은 Pod 객체로 접근할 수 있어요. 즉 원하는 블록과 같은 이름에 = 트위길을 붙인 변수예요.
=begin Foo
...
=end Foo
# 이후에 $=Foo가 모든 Foo-Pod-블록을 줌
모든 Pod 구조를 계층적 데이터 구조로 담은 Pod 트리는 $=pod로 접근할 수 있어요.
그런 $=someBlockName 변수들은 모두 Positional과 Associative role을 지원한다는 점을 주의하세요.
~ 트위길
~ 트위길은 하위 언어(sublanguage, slang)를 가리키는 데 써요. 유용한 것들은 다음과 같아요.
| $~MAIN | 현재 메인 언어 (예: Raku 문) |
|---|---|
| $~Quote | 따옴표 언어의 현재 루트 |
| $~Quasi | 준따옴표(quasiquoting) 언어의 현재 루트 |
| $~Regex | 정규식 언어의 현재 루트 |
| $~Trans | 음역(transliteration) 언어의 현재 루트 |
| $~P5Regex | 펄 정규식 언어의 현재 루트 |
이 언어들을 현재 어휘 스코프에서 augment할 수 있어요.
use MONKEY-TYPING;
augment slang Regex { # $~Regex에서 파생한 뒤 $~Regex 수정
token backslash:std<\Y> { YY };
}
자세한 내용은 slangs 문서를 참고하세요.
변수 선언자와 스코프
대부분은 my 키워드로 새 변수를 만드는 것으로 충분해요.
my $amazing-variable = "World";
say "Hello $amazing-variable!"; # OUTPUT: «Hello World!»
하지만 트위길이 할 수 있는 것 이상으로 스코프 세부를 바꾸는 선언자가 여럿 있어요.
| 선언자 | 효과 |
|---|---|
| my | 어휘 스코프 이름 도입 |
| our | 패키지 스코프 이름 도입 |
| has | 애트리뷰트 이름 도입 |
| anon | 구성에 비공개인 이름 도입 |
| state | 어휘 스코프지만 지속되는 이름 도입 |
| constant | 컴파일 타임에 불변으로 묶이는 이름 도입 |
| augment | 기존 이름에 정의 추가 |
| supersede | 기존 이름의 정의 대체 |
사전 정의된 변수에 작용하는 키워드도 두 개 있어요:
| 키워드 | 효과 |
|---|---|
| temp | 스코프 끝에서 변수 값 복원 |
| let | 블록이 실패로 끝나면 스코프 끝에서 변수 값 복원 |
my 선언자
my로 변수를 선언하면 어휘 스코프가 돼요. 즉 현재 블록 안에서만 존재해요. 예:
{
my $foo = "bar";
say $foo; # OUTPUT: «"bar"»
}
say $foo; # 예외! "Variable '$foo' is not declared"
$foo가 같은 스코프 안에 있을 때만 정의되기 때문에 이 코드는 죽어요.
같은 문장에서 어휘 스코프의 변수를 두 개 이상 만들려면 변수를 괄호로 감싸세요:
my ( $foo, $bar );
어휘 스코프 또는 패키지 스코프의 변수 목록 선언 섹션도 참고하세요.
추가로 어휘 스코프는 변수를 새 스코프에서 임시로 재정의할 수 있게 해줘요:
my $location = "outside";
sub outer-location {
# 재정의되지 않음:
say $location;
}
outer-location; # OUTPUT: «outside»
sub in-building {
my $location = "inside";
say $location;
}
in-building; # OUTPUT: «inside»
outer-location; # OUTPUT: «outside»
변수가 재정의됐다면 바깥 변수를 참조했던 코드는 계속 바깥 변수를 참조해요. 그래서 여기 &outer-location은 여전히 바깥 $location을 출력해요:
sub new-location {
my $location = "nowhere";
outer-location;
}
new-location; # OUTPUT: «outside»
new-location()이 nowhere를 출력하게 하려면 $location을 * 트위길로 동적 변수로 만들면 돼요. 이 트위길은 지역 스코프를 시도한 뒤 심볼을 바깥 스코프가 아니라 호출 스코프에서 조회하도록 컴파일러에 지시해요.
my는 서브루틴의 기본 스코프라서 my sub x() {}와 sub x() {}는 완전히 같은 동작을 해요.
이것 덕분에 컴파일러가 런타임 전에 여러 가지를 처리하기 쉬워져요. 다음을 포함해요:
- 서브루틴 이름을 읽기 전용으로 결합(
constant와 유사) - 선언되지 않은 서브루틴 보고
- 인자 검사
- 다중 디스패치 해석
- 더 자세한 오류 메시지
- 최적화
일반적으로 항목의 가시성이 코드에서의 위치와 직접 연관되면 프로그램을 이해하고 추론하기 쉬워져서, 컴파일러뿐 아니라 IDE·디버거 같은 다른 도구도 더 유용해져요.
또 한 가지 주의할 점은, 상수·열거·모듈·여러 종류의 타입(클래스, 서브셋, 그램마, role) 선언은 모두 기본적으로 패키지 스코프(아래 our 참고)인데, 그런 선언에 my를 붙이면 API에 노출되지 않게 해서, 의존자를 깨뜨리지 않고 향후 개정에서 수정할 자유를 얻을 수 있어요. 예:
class Foo { # GLOBAL에 보임
my class ImplementationDetail { # Foo 사용자에게 숨겨짐
# ...
}
# ...
}
our 선언자
our 변수는 둘러싼 패키지의 스코프에서 만들어져요. 어휘 스코프에도 별칭을 만들어서 my 변수처럼 쓸 수도 있어요.
module M {
our $Var;
# $Var는 여기서 사용 가능
}
# 여기서는 $M::Var로 사용 가능
my로 어휘 스코프를 쓰는 게 장점이 많지만, 때로는 주어진 어휘 스코프보다 더 넓게 공유해야 할 때가 있어요. is export 트레이트를 쓰면 네임스페이스 충돌이 생길 수 있고요. our로 패키지 스코프를 쓰면 심볼을 서로 다른 네임스페이스에 할당해 충돌을 피해요. 예를 들어 같은 코드에서 HTTP와 WebSocket용 Cro 클라이언트를 모두 쓰려면 각각 Cro::HTTP::Client, Cro::WebSocket::Client로 참조하면 돼요.
패키지는 class, module, grammar, (조건이 있지만) role 같은 패키지 선언자로 도입돼요. our 선언은 둘러싼 패키지 구성에 설치돼요. 그러면 코드 사용자는(use로) 패키지의 our 스코프 항목을 완전한 패키지 정규화 이름으로 참조할 수 있어요.
모든 패키지는 GLOBAL이라는 최상위 패키지 안에 존재하므로 전역으로 보여요. 그래서 our 스코프 변수를 선언하는 것은 네임스페이스 제어가 있어도 전역 변수를 선언하는 거예요. GLOBAL을 통해 보이게 되는 모든 것은 사실상 당신의 API의 일부이므로 그에 맞게 고려해야 해요.
가장 공유될 가능성이 높은 요소들은 기본적으로 패키지(our) 스코프예요:
class,role,grammar,subset을 포함한 타입 선언- 상수
- 열거
module선언
기본적으로 패키지 스코프가 아닌 것들:
- 서브루틴은 앞서 논의했듯 기본적으로 어휘(
my) 스코프 - 메서드는
has로 스코프됨(메서드 디스패치를 통해서만 보임) - 변수는 기본 스코프가 없음 (가장 흔한 선택인
my가 입력하기에도 가장 짧지만)
패키지 스코프의 변수를 한 번에 두 개 이상 만들려면 변수를 괄호로 감싸세요:
our ( $foo, $bar );
어휘·패키지 스코프의 변수 목록 선언 섹션도 참고하세요.
스코프 팁: my vs our
요소를 기본값보다 덜 보이게 만드는 것(예: 내부용 상수나 구현 세부를 구조화하는 클래스에 my 쓰기)은 좋은 생각일 수 있어요. API에 무언가를 노출해야 할 때는 최소한 세 가지 방법이 있다는 걸 기억하세요:
our스코프 요소: 패키지 네임스페이스가 접근을 제어sub:is export트레이트로 보이고,sub EXPORT로 잠재적 접근 제어method: 패키지 스코프class를 통해 보임
정리하면:
- 구현 세부인 모든 것에는
my스코프를 쓰세요 export할 계획인 것에도my를 쓰되, export는 심볼을 소비자의 단일 어휘 스코프에 넣어 이름 충돌 위험이 있으니 특히 일반적인 이름 export에 주의하세요- 공유하려는 것, 그리고 네임스페이스로 충돌을 피하고 싶을 때
our를 쓰세요 - 공유하고 싶은 요소들은 어차피 기본적으로
our스코프이므로, 명시적으로our를 쓰는 것은 한 번 더 생각해 볼 이유가 돼요
어휘(my) 또는 패키지(our) 스코프 변수 목록 선언
한 번에 변수 여러 개에 스코프를 적용할 수 있는데, my와 our 모두 변수를 괄호에 넣어야 해요:
my (@a, $s, %h); # same as my @a; my $s; my %h;
our (@aa, $ss, %hh); # same as our @aa; our $ss; our %hh;
이것은 구조분해 할당과 함께 쓸 수 있어요. 그런 목록에 대한 할당은 왼쪽 목록이 제공하는 요소 수만큼 오른쪽 목록의 해당 값을 할당해요. 빠진 요소는 변수 타입에 따라 정의되지 않은 값이 돼요.
my (Str $a, Str $b, Int $c) = <a b>;
say [$a, $b, $c].raku;
# OUTPUT: «["a", "b", Int]»
목록을 단일 값으로 구조분해하려면 ($var,)로 요소가 하나인 목록 리터럴을 만들어요. 변수 선언자와 쓰면 단일 변수 주위에 괄호만 제공하면 충분해요.
sub f { 1,2,3 };
my ($a) = f;
say $a.raku;
# OUTPUT: «1»
목록의 요소를 건너뛰려면 익명 상태 변수 $를 써요.
my ($,$a,$,%h) = ('a', 'b', [1,2,3], {:1th});
say [$a, %h].raku;
# OUTPUT: «["b", {:th(1)}]»
has 선언자
has는 애트리뷰트를 클래스나 role의 인스턴스에, 메서드를 클래스나 role에 스코프해요. 메서드에는 has가 암묵적이라서 has method x() {}와 method x() {}는 같은 일을 해요.
자세한 문서와 예시는 객체 지향 문서를 참고하세요.
anon 선언자
anon 선언자는 심볼이 어휘 스코프, 메서드 테이블, 그리고 그 밖의 어디에도 설치되지 않게 막아요.
예를 들어 자신의 이름은 알지만 스코프에는 설치되지 않는 서브루틴을 선언할 때 쓸 수 있어요:
my %operations =
half => anon sub half($x) { $x / 2 },
square => anon sub square($x) { $x * $x },
;
say %operations<square>.name; # square
say %operations<square>(8); # 64
선언자이므로 무언가가 선언될 수 있는 어디에나 적용할 수 있어요. 예를 들어 클래스나 시길 없는 변수에도요.
say anon class þ {}; # OUTPUT: «(þ)»
say anon sub þ { 42 }; # OUTPUT: «&þ»
이 심볼들은 스코프에 설치되지 않으므로 이름으로 쓸 수 없어요. 하지만 외부 변수에 할당해야 하고 스스로의 이름을 알아야 한다면 유용해요. 스스로의 이름은 인트로스펙션으로 얻을 수 있고요.
my $anon-class = anon class {
has $.bar;
method equal( ::?CLASS $foo ) {
return $foo.bar == $.bar;
}
};
say $anon-class.new( :3bar).equal( $anon-class.new( :3bar ) );
# OUTPUT: «True»
state 선언자
state는 my처럼 어휘 스코프 변수를 선언해요. 하지만 초기화는 정확히 한 번, 정상 실행 흐름에서 초기화를 처음 만날 때 일어나요. 그래서 state 변수는 둘러싼 블록이나 루틴을 여러 번 실행해도 값을 유지해요.
따라서 다음 서브루틴은:
sub a {
state @x;
state $l = 'A';
@x.push($l++);
};
say a for 1..6;
호출될 때마다 $l을 계속 증가시키고 @x에 덧붙여요. 매 호출마다 글자가 하나씩 늘어나고, 그래서 출력은 아래처럼 한 요소씩 자라는 목록이 돼요:
[A]
[A B]
[A B C]
[A B C D]
[A B C D E]
[A B C D E F]
그들은 어휘 스코프를 가지므로 선언된 블록에 묶여요.
sub foo () {
for 0..1 {
state $foo = 1;
say $foo++;
}
};
foo; # OUTPUT: «12»
foo; # OUTPUT: «12»
이 경우 for 루프를 실행하는 블록에 들어갈 때마다 새 state 변수가 생겨서, foo를 호출할 때마다 state 변수가 초기화돼요.
이것은 포함된 코드 객체의 "복제본(clone)"마다 동작해요. 이 예시에서처럼요:
({ state $i = 1; $i++.say; } xx 3).map: {$_(), $_()}; # OUTPUT: «121212»
같은 블록의 같은 복제본을 여러 스레드가 실행하면 스레드에 안전하지 않다는 점에 주의하세요. 또 메서드는 객체마다가 아니라 클래스마다 복제본이 하나라는 것도 기억하세요.
my처럼 여러 state 변수의 선언은 괄호에 넣어야 하는데, 단일 변수는 괄호를 생략할 수 있어요.
많은 연산자는 암묵적 결합을 수반해 원거리 작용(action at a distance)을 일으킬 수 있어요.
결합할 수 있는 새 컨테이너를 만들려면 .clone이나 강제 변환(coercion)을 쓰세요.
my @a;
my @a-cloned;
sub f() {
state $i;
$i++;
@a .push: "k$i" => $i;
@a-cloned.push: "k$i" => $i.clone;
};
f for 1..3;
say @a; # OUTPUT: «[k1 => 3 k2 => 3 k3 => 3]»
say @a-cloned; # OUTPUT: «[k1 => 1 k2 => 2 k3 => 3]»
state 변수는 모든 스레드 간에 공유돼요. 결과가 예상과 다를 수 있어요.
sub code(){ state $i = 0; say ++$i; $i };
await
start { loop { last if code() >= 5 } },
start { loop { last if code() >= 5 } };
# OUTPUT: «1234435»
# OUTPUT: «21345»
# 그 외에도 다양하고 다소 이상한 변형이 생길 수 있음
$ 변수
명시적으로 선언한 이름 있는 state 변수 외에도, $는 명시적인 state 선언 없이 익명 상태 변수로 쓸 수 있어요.
say "1-a 2-b 3-c".subst(:g, /\d/, {<one two three>[$++]});
# OUTPUT: «one-a two-b three-c»
또한 state 변수는 서브루틴 밖에서도 쓸 수 있어요. 예를 들어 한 줄짜리 코드에서 $를 써서 파일의 줄에 번호를 매길 수 있어요.
raku -ne 'say ++$ ~ " $_"' example.txt
어휘 스코프 안의 $에 대한 각 참조는 사실상 별개의 변수예요.
raku -e '{ say ++$; say $++ } for ^5'
# OUTPUT: «1021324354»
그래서 같은 $ 변수(또는 다른 익명 상태 변수 @, %)를 두 번 이상 참조해야 한다면 다른 변수를 여기에 결합하는 게 가능한 해법이에요. 다만 이 예시에서는 그냥 state $x를 선언하고 마법적인/익명 $ 변수를 쓰지 않는 게 더 간단해요:
sub foo () {
my $x := $;
$x++;
say $x;
$x = $x + 1;
}
foo() for ^3; # OUTPUT: «135»
일반적으로 여러 번 참조해야 한다면 이름 있는 state 변수를 선언하는 게 더 나은 스타일이에요.
암묵적 state 선언자는 초기화를 포함한 표현식이 아니라 변수 자체에만 적용된다는 점을 주의하세요. 초기화를 정확히 한 번 호출해야 한다면 state 선언자를 제공해야 해요.
for ^3 { $ = .say } # OUTPUT: «012»
for ^3 { state $ = .say } # OUTPUT: «0»
@ 변수
$ 변수와 비슷하게 Positional 익명 상태 변수 @도 있어요.
sub foo($x) {
say (@).push($x);
}
foo($_) for ^3;
# OUTPUT: «[0]
# [0 1]
# [0 1 2]»
여기 @는 @.push라는 클래스 멤버 변수와의 혼동을 피하기 위해 괄호로 감쌌어요. 인덱스 접근에는 이 괄호가 필요 없지만, 그 값으로 유용한 일을 하려면 값을 복사해야 해요.
sub foo($x) {
my $v = @;
$v[$x] = $x;
say $v;
}
foo($_) for ^3;
# OUTPUT: «[0]
# [0 1]
# [0 1 2]»
$처럼 스코프 안의 @를 언급할 때마다 새 익명 배열이 도입돼요.
% 변수
추가로 Associative 익명 상태 변수 %가 있어요.
sub foo($x) {
say (%).push($x => $x);
}
foo($_) for ^3;
# OUTPUT: «{0 => 0}
# {0 => 0, 1 => 1}
# {0 => 0, 1 => 1, 2 => 2}»
괄호가 필요한 것에 대한 같은 주의가 적용돼요. 예상대로 인덱스 접근도 가능해요(유용하게 쓰려면 복사 필요).
sub foo($x) {
my $v = %;
$v{$x} = $x;
say $v;
}
foo($_) for ^3;
# OUTPUT: «{0 => 0}
# {0 => 0, 1 => 1}
# {0 => 0, 1 => 1, 2 => 2}»
다른 익명 상태 변수처럼 주어진 스코프에서 %를 언급할 때마다 사실상 별개의 변수가 도입돼요.
constant 선언자
constant 선언자는 태그된 값이 수명 동안 변하지 않을 것임을 선언해요.
constant $pi2 = pi * 2;
$pi2 = 6; # OUTPUT: «(exit code 1) Cannot assign to an immutable value
값은 컴파일 타임에 할당돼요. Raku 모듈은 자동으로 사전 컴파일되므로 모듈에 정의된 상수는 프로그램 실행 시 재평가되지 않아요. 추가 정보는 Terms 페이지의 상수 섹션을 확인하세요.
augment 선언자
augment로 기존 클래스·그램마에 (애트리뷰트는 안 되고) 메서드를 추가할 수 있어요. 단, 먼저 MONKEY-TYPING 프래그마를 활성화해야 해요.
클래스는 보통 our 스코프라서 전역이므로, 이는 전역 상태를 수정한다는 뜻이고 강력히 권장되지 않아요. 거의 모든 상황에서 더 나은 해법이 있어요.
# 이렇게 하지 마세요
use MONKEY-TYPING;
augment class Int {
method is-answer { self == 42 }
}
say 42.is-answer; # OUTPUT: «True»
(이 경우 더 나은 해법은 함수를 쓰는 거예요.)
더 나으면서 안전한 예시로, IO::Path에 현재 없어서 아쉬운, extension을 제거하고 남은 basename 부분을 돌려주는 메서드를 추가해 IO::Path를 확장하는 클래스 모듈을 만드는 실용적인 방법이 있어요. (참고: 그 부분을 뭐라고 부르고 어떻게 구성해야 하는지에 대한 개발자 합의는 명확하지 않아요.)
unit class IO::Barename is IO::Path;
method new(|c) {
return self.IO::Path::new(|c);
}
use MONKEY-TYPING;
augment class IO::Path {
method barename {
self.extension("").basename;
}
}
temp 키워드
temp는 my처럼 스코프 끝에서 변수의 옛 값을 복원해요. 하지만 temp는 새 변수를 만들지 않아요.
my $in = 0; # temp는 전역 변수를 호출 스택과 "얽어"서
# 호출을 바닥에서 순서대로 유지함
sub f(*@c) {
(temp $in)++;
"<f>\n"
~ @c».indent($in).join("\n")
~ (+@c ?? "\n" !! "")
~ '</f>'
};
sub g(*@c) {
(temp $in)++;
"<g>\n"
~ @c».indent($in).join("\n")
~ (+@c ?? "\n" !! "")
~ "</g>"
};
print g(g(f(g()), g(), f()));
# OUTPUT: «<g>
# <g>
# <f>
# <g>
# </g>
# </f>
# <g>
# </g>
# <f>
# </f>
# </g>
# </g>»
let 키워드
블록이 실패로 끝나면 이전 값을 복원해요. 성공적 종료란 블록이 정의된 값이나 목록을 반환했다는 뜻이에요.
my $answer = 42;
{
let $answer = 84;
die if not Bool.pick;
CATCH {
default { say "it's been reset :(" }
}
say "we made it 84 sticks!";
}
say $answer;
위 경우 Bool.pick이 true를 반환하면 say가 True를 반환해 블록이 정의된 값을 반환하므로 answer는 84로 유지돼요. 그렇지 않으면 die 문이 블록을 실패로 끝내게 해서 answer를 42로 재설정해요.
타입 제약과 초기화
변수는 묶인 컨테이너를 통해 타입 제약을 가지며, 이는 선언자와 변수 이름 사이에 와요. 기본 타입 제약은 Mu예요. of 트레이트로 타입 제약을 설정할 수도 있어요.
my Int $x = 42;
$x = 'a string';
CATCH { default { put .^name, ': ', .Str } }
# OUTPUT: «X::TypeCheck::Assignment: Type check failed in assignment to $x; expected Int but got Str ("a string")»
스칼라 변수에 타입 제약이 있는데 초기 값이 없으면, 묶인 컨테이너의 기본 값의 타입 객체로 초기화돼요.
my Int $x;
say $x.^name; # OUTPUT: «Int»
say $x.defined; # OUTPUT: «False»
명시적 타입 제약이 없는 스칼라 변수는 Mu로 타입되지만 기본적으로 Any 타입 객체로 설정돼요.
@ 시길이 있는 변수는 빈 Array로, % 시길이 있는 변수는 빈 Hash로 초기화돼요.
변수의 기본 값은 is default 트레이트로 설정할 수 있고, 그 값에 Nil을 할당하면 다시 적용돼요:
my Real $product is default(1);
say $product; # OUTPUT: «1»
$product *= 5;
say $product; # OUTPUT: «5»
$product = Nil;
say $product; # OUTPUT: «1»
기본 정의 변수 프래그마
모든 변수가 확정(definiteness) 제약을 갖게 하려면 프래그마 use variables :D를 써요. 프래그마는 어휘 스코프이고 use variables :_로 끌 수 있어요.
use variables :D;
my Int $i;
# OUTPUT: «===SORRY!=== Error while compiling <tmp>Variable definition of type Int:D (implicit :D by pragma) requires an initializer ...
my Int $i = 1; # that works
{ use variables :_; my Int $i; } # 이 블록에서는 끔
Nil을 할당하면 변수가 기본 값으로 되돌아가는데, 흔히 확정 값이 아니라서 제약을 실패시킨다는 점에 주의하세요:
use variables :D;
my Int $x = 42;
$x = Nil;
# OUTPUT: «Type check failed in assignment to $x; expected type Int:D cannot be itself…»
이름이 시사하듯 이 프래그마는 변수에만 적용돼요.
특수 변수 (Special variables)
Raku는 특수 변수에 길고 설명적인 이름을 쓰려고 해요. 유난히 짧은 특수 변수는 세 개뿐이에요.
사전 정의된 어휘 변수
항상 사용 가능한 특수 변수는 네 개예요.
| 변수 | 의미 | 스코프 |
|---|---|---|
| $_ | 주제 변수(topic) | 모든 블록 |
| $/ | 정규식 매치 | 모든 sub / method |
| $! | 예외 | 모든 sub / method |
| $¢ | 마지막 매치 ($/와 유사) | 현재 정규식 내부 |
정규식 밖에서 $¢에 접근하면 오류 없이 접근할 수 있지만 Nil을 담는다는 점을 주의하세요.
$_ 변수
$_는 주제 변수(topic variable)예요. 모든 블록에서 새 것이 만들어져요. 명시적 시그니처가 없는 블록의 기본 매개변수이기도 해서, for @array { ... }나 given $var { ... } 같은 구성은 블록을 호출해 변수의 값(들)을 $_에 결합해요.
for <a b c> { say $_ } # $_를 'a', 'b', 'c'에 차례로 결합
say $_ for <a b c>; # 블록이 아니어도 동일
given 'a' { say $_ } # $_를 'a'에 결합
say $_ given 'a'; # 블록이 아니어도 동일
$_는 반복의 값에 결합되므로, 할당 가능한 것에 결합돼 있다면 $_에 할당할 수도 있어요.
my @numbers = ^5; # 0부터 4까지
$_++ for @numbers; # @numbers의 모든 요소 증가
say @numbers;
# OUTPUT: «1 2 3 4 5»
CATCH 블록은 잡힌 예외를 $_에 결합해요. ~~ 스마트매치 연산자는 오른쪽 표현식의 $_를 왼쪽 값에 결합해요.
$_에 메서드를 호출하는 것은 변수 이름을 생략해 줄일 수 있어요:
.say; # same as $_.say
m/regex/와 /regex/ 정규식 매치, s/regex/subst/ 치환은 $_에 대해 동작해요:
say "Looking for strings with non-alphabetic characters...";
for <ab:c d$e fgh ij*> {
.say if m/<-alpha>/;
}
# OUTPUT: «Looking for strings with non-alphabetic characters...
# ab:c
# d$e
# ij*»
$/ 변수
$/는 매치 변수예요. 모든 루틴에서 새 것이 만들어져요. 마지막 정규식 매치의 결과로 설정되므로 보통 Match 타입 객체를 담아요.
'abc 12' ~~ /\w+/; # $/를 Match 객체로 설정
say $/.Str; # OUTPUT: «abc»
Grammar.parse 메서드도 호출자의 $/를 결과 Match 객체로 설정해요. 다음 코드에서:
use XML::Grammar; # zef install XML
XML::Grammar.parse("<p>some text</p>");
say $/;
# OUTPUT: «「<p>some text</p>」
# root => 「<p>some text</p>」
# name => 「p」
# child => 「some text」
# text => 「some text」
# textnode => 「some text」
# element => 「<p>some text</p>」
# name => 「p」
# child => 「some text」
# text => 「some text」
# textnode => 「some text」»
6.d 버전 이전에는 $() 단축키로 $/ Match에서 ast 값을 (그 값이 truthy면) 얻거나, 아니면 Match 객체의 문자열화를 얻을 수 있었어요.
'test' ~~ /.../;
# 6.c language only:
say $(); # OUTPUT: «tes»;
$/.make: 'McTesty';
say $(); # OUTPUT: «McTesty»;
이 (비)기능은 6.d 버전부터 deprecated됐어요.
위치 애트리뷰트
정규식에 캡처 그룹(그냥 괄호로 만든 것)이 있으면 $/는 위치 애트리뷰트를 가질 수 있어요.
'abbbbbcdddddeffg' ~~ / a (b+) c (d+ef+) g /;
say $/[0]; # OUTPUT: «「bbbbb」»
say $/[1]; # OUTPUT: «「dddddeff」»
이것들은 $0, $1, $2 등의 단축키로도 접근할 수 있어요.
say $0; # OUTPUT: «「bbbbb」»
say $1; # OUTPUT: «「dddddeff」»
위치 애트리뷰트를 모두 얻으려면 $/.list나 @$/를 써요. 6.d 이전에는 @() 단축키(괄호 안에 공백 없음)도 쓸 수 있었어요.
say @$/.join; # OUTPUT: «bbbbbdddddeff»
# 6.c language only:
say @().join; # OUTPUT: «bbbbbdddddeff»
@()의 이 마법 같은 동작은 6.d부터 deprecated됐어요.
이름 애트리뷰트
정규식에 이름 있는 캡처 그룹이 있거나, 정규식이 다른 정규식을 호출했다면 $/는 이름 애트리뷰트를 가질 수 있어요.
'I... see?' ~~ / \w+ $<punctuation>=[ <-[\w\s]>+ ] \s* $<final-word> = [ \w+ . ] /;
say $/<punctuation>; # OUTPUT: «「....」»
say $/<final-word>; # OUTPUT: «「see?」»
이것들은 $<named> 단축키로도 접근할 수 있어요.
say $<punctuation>; # OUTPUT: «「....」»
say $<final-word>; # OUTPUT: «「see?」»
이름 애트리뷰트를 모두 얻으려면 $/.hash나 %$/를 써요. 6.d 이전에는 %() 단축키(괄호 안에 공백 없음)도 쓸 수 있었어요.
say %$/.join; # OUTPUT: «"punctuation ....final-word see?"»
# 6.c language only
say %().join; # OUTPUT: «"punctuation ....final-word see?"»
이 동작은 6.d 버전부터 deprecated됐어요.
스레드 안전성 문제
$/는 루틴마다 정의되므로, 루프에서 매칭을 하면 사실상 같은 $/를 재사용하는 거예요. 단일 스레드 프로그램에서는 문제가 없어요. 하지만 hyper나 race로 여러 스레드가 병렬 매칭을 하게 되면 "바깥" $/의 공유가 문제가 돼요. 스레드 간에 공유되기 때문이죠! 다행히 해법은 아주 간단해요. 매칭을 하는 스코프 안에서 자신만의 $/를 정의하면 돼요. 예를 들어 텍스트 소스를 가져와 정규식을 실행하고 병렬 실행으로 해시에 매핑한다면:
my %mapped = @source.race.map: {
my $/; # 경합 문제를 막기 위해 이 블록에 하나가 필요
m/foo (.*?) bar (.*)/; # $_에 매칭, $/에 저장
$0 => $1 # $/[0] / $/[1]의 축약
}
$! 변수
$!는 오류 변수예요. 모든 루틴에서 새 것이 만들어져요. try 블록이나 문 접두어가 예외를 잡으면 그 예외가 $!에 저장돼요. 예외를 잡지 못했다면 $!는 Nil로 설정돼요.
CATCH 블록은 $!를 설정하지 않는다는 점에 주의하세요. 대신 블록 안의 $_를 잡힌 예외로 설정해요.
$! 사용에도 $/와 같은 스레드 안전성 문제가 적용돼요.
$¢ 변수
Match에 있는 설명을 참고하세요.
컴파일 타임 변수
모든 컴파일 타임 변수는 트위길의 일부로 물음표를 가져요. 컴파일 타임이므로 런타임에 바꿀 수 없지만, 프로그램을 인트로스펙션하는 데 귀중해요. 가장 흔한 컴파일 타임 변수들은 다음과 같아요.
| $?FILE | 내가 어느 파일에 있나? |
|---|---|
| $?LINE | 내가 어느 줄에 있나? [1부터 셈] |
| ::?CLASS | 내가 어느 클래스에 있나? |
| %?RESOURCES | 현재 컴파일 유닛의 "Distribution"과 연관된 파일들 |
$?FILE와 $?LINE은 CallFrame에서 각각 file과 line 메서드로도 사용 가능해요.
%?RESOURCES
%?RESOURCES는 Distribution의 코드에서 사용 가능한 컴파일 타임 변수예요.
현재 컴파일 유닛의 Distribution과 연관된 파일에 컴파일·실행 시 접근을 제공하는 해시를 담아요. 이 해시는 Distribution 전체의 정적 파일(설정 파일, 템플릿, 데이터 파일 예시)을 위한 특별 저장소에 접근하는 데 써요.
이 변수로 사용 가능한 파일은 Distribution의 resources 디렉터리 아래에 놓아야 해요:
Module-Foo/
├── lib
│ └── Module
│ └── Foo.rakumod
├── META6.json
├── README.md
└── resources
└── images
└── foo.jpg
추가로 (배포판의 루트 resources 디렉터리에서 시작하는) 상대 경로를 META6.json 파일의 "resources" 필드에 지정할 수 있어요:
"resources": [
"images/foo.jpg"
]
모든 리소스 파일은 설치된 Distribution에 추가되고 %?RESOURCES에 Hash처럼 접근해 Distribution::Resource 객체를 반환해요:
my $foo-IO = %?RESOURCES<images/foo.jpg>; # slurp할 수 있는 객체를 얻음
my $foo-IO = %?RESOURCES<images/foo.jpg>.open; # 작업할 열린 IO::Handle을 얻음
설치된 distribution에서 리소스 파일의 경로와 이름이 변형될 수 있으므로, %?RESOURCES 변수의 키로 쓰는 경우 외에는 그 값에 의존하지 마세요.
%?RESOURCES 변수는 평범한 Hash가 아니라 Distribution::Resource 타입의 인스턴스로 구현돼요. 그래서 출력하거나 다른 방식으로 값을 검사해 distribution의 모든 리소스 파일을 보길 기대하지 마세요. 대신 위에 설명한 API로 특정 파일에 접근해야 해요.
%?RESOURCES 변수는 모듈 안에서만 접근할 수 있어요. 모듈 밖에서 %?RESOURCES에 접근하려면 그 API를 직접 노출해야 해요. 한 가지 방법은 lib 디렉터리에 그 값을 반환하는 루틴을 만드는 거예요:
unit module MyLib;
sub my-resources is export {
%?RESOURCES
}
그다음 t/resources.t 같은 테스트 파일을 만들어요:
use Test;
use MyLib;
my $resources = my-resources;
isa-ok $resources, Distribution::Resource;
이렇게 컴파일 타임 해시의 내용이 런타임 코드에 노출돼요.
인트로스펙션 컴파일 타임 변수
더 깊은 인트로스펙션을 허용하는 컴파일 타임 변수들:
| $?PACKAGE | 내가 어느 패키지에 있나? |
|---|---|
| $?MODULE, ::?MODULE | 내가 어느 모듈에 있나? 모듈의 타입을 담음 |
| $?CLASS | 내가 어느 클래스에 있나? (변수로) |
| $?ROLE | 내가 어느 role에 있나? (변수로) |
| $?TABSTOP | heredoc이나 가상 여백에서 탭은 공백 몇 칸인가? |
| $?NL | 세로줄바꿈 "\n"이 의미하는 것: LF, CR 또는 CRLF |
| $?DISTRIBUTION | 현재 컴파일 유닛의 Distribution |
특히 $?NL에 관해서는 newline 프래그마를 참고하세요.
Rakudo 특유의 컴파일 타임 변수
이 변수들은 Rakudo 특유이며, 모든 대응하는 주의점이 적용돼요:
| $?BITS | 프로그램이 컴파일되는 플랫폼의 데이터 경로 비트 수 |
|---|
&?ROUTINE
컴파일 타임 변수 &?ROUTINE은 프로그램이 실제로 어떤 루틴 안에 있는지에 대한 인트로스펙션을 제공해요. 현재 루틴에 붙은 Routine 인스턴스를 반환해요. 호출된 루틴의 이름을 얻는 .name 메서드와 .signature 및 Routine과 관련된 다른 메서드를 지원해요:
sub awesome-sub { say &?ROUTINE.name }
awesome-sub; # OUTPUT: «awesome-sub»
재귀도 허용해요:
my $counter = 10;
sub do-work {
say 'Calling myself other ' ~ $counter-- ~ ' times';
&?ROUTINE() if ( $counter > 0 );
}
do-work;
multi에서는 &?ROUTINE이 multi 전체가 아니라 현재 후보(candidate)를 가리킨다는 점에 주의하세요.
그래서 다음 재귀 정의는 동작하지 않아요:
multi broken-fibonacci($n where * ≤ 1) { $n }
multi broken-fibonacci($n where * > 0) {
&?ROUTINE($n - 1) + &?ROUTINE($n - 2)
}
호출하면 &?ROUTINE은 항상 두 번째 multi 후보를 가리켜 첫 번째로 디스패치되지 않아요. 전체 proto에 대해 자기 재귀를 쓰려면 함수 이름이나 samewith를 쓰세요.
&?BLOCK
특수 컴파일 변수 &?BLOCK은 &?ROUTINE과 비슷하게 동작하지만 단일 코드 블록을 인트로스펙션할 수 있게 해줘요. Block을 담고 같은 블록 안에서 재귀를 허용해요:
for '.' {
.Str.say when !.IO.d;
.IO.dir()».&?BLOCK when .IO.d # 약간 재귀하게 함!
}
$?DISTRIBUTION
$?DISTRIBUTION은 현재 컴파일 유닛의 Distribution에 접근을 제공해요. 이는 모듈 작성자가 distribution의 다른 파일을 원래 상대 경로 이름으로 참조하거나, (CompUnit::Repository::Installation이 설치 시 파일 레이아웃을 어떻게 바꾸는지 같은) 밑바닥 파일 구조를 알 필요 없이 (.meta 메서드로) 메타데이터를 보게 해줘요.
unit module MyFoo;
sub module-version {
say "MyFoo is version:";
say $?DISTRIBUTION.meta<ver>; # OUTPUT: «0.0.1»
}
.meta 메서드는 META6.json의 값, 특히 version, auth, api에 접근을 제공해요.
.content 메서드는 distribution 최상위 디렉터리에서의 상대 경로가 주어지면 distribution 안의 파일에 접근을 제공해요.
$?DISTRIBUTION은 모듈 안에서 쓸 때만 정보를 반환해요. 스크립트에서는 Nil을 담아요.
sub module-source {
say "MyFoo source code:";
say $?DISTRIBUTION.content('lib/MyFoo.rakumod');
# OUTPUT: «IO::Handle<"lib/MyFoo.rakumod".IO>(closed)»
}
content 예시의 출력은 닫힌 파일 핸들이라는 점에 주의하세요. 실제로 파일 내용을 쓰려면 같은 모듈의 다른 루틴에서 문자열로 추출할 수 있어요:
sub get-file-content {
say $?DISTRIBUTION.content('lib/MyFoo.rakumod').open.slurp;
# OUTPUT: «unit module MyFoo;»
}
동적 변수 (Dynamic variables)
사전 정의된 동적 스코프 변수들은 모두 * 트위길을 가지며, 이름은 (관례상) 대문자로 써요.
인자 관련 변수
이 변수들은 스크립트에 전달된 인자와 관련돼요.
$*ARGFILES
@*ARGS를 소스 파일로 쓰는 IO::ArgFiles(IO::CatHandle의 빈 하위 클래스)로, 파일이 있으면 그것을, 아니면 $*IN을 사용해요. $*IN이 쓰일 때 그 :nl-in, :chomp, :encoding, :bin이 IO::ArgFiles 객체에 설정돼요.
6.d 버전부터 sub MAIN 안의 $*ARGFILES는 @*ARGS가 비어 있지 않아도 항상 $*IN으로 설정돼요. 예시와 더 많은 맥락은 클래스 문서를 참고하세요.
@*ARGS
@*ARGS는 명령행의 인자를 담는 Str 배열이에요.
&*ARGS-TO-CAPTURE
어떤 맞춤 ARGS-TO-CAPTURE 서브루틴 안에서도 사용 가능한 동적 변수로, 기본 인자 파싱을 수행하는 데 쓸 수 있어요. 맞춤 ARGS-TO-CAPTURE 서브루틴이 기대하는 것과 같은 매개변수를 받아요.
&*GENERATE-USAGE
어떤 맞춤 GENERATE-USAGE 서브루틴 안에서도 사용 가능한 동적 변수로, 기본 사용법 메시지 생성을 수행하는 데 쓸 수 있어요. 맞춤 GENERATE-USAGE 서브루틴이 기대하는 것과 같은 매개변수를 받아요.
특수 파일 핸들: STDIN, STDOUT, STDERR
특수 파일 핸들에 대한 자세한 내용은 입력·출력 페이지와 IO::Special 클래스를 참고하세요. IO::Handle에는 표준 입력을 읽기 위해 $*IN을 쓰는 예시가 여러 개 있어요.
$*IN표준 입력 파일 핸들, STDIN.$*OUT표준 출력 파일 핸들, STDOUT.$*ERR표준 오류 파일 핸들, STDERR.
런타임 환경
이 동적 변수들은 스크립트·프로그램이 실행되는 환경과 관련된 정보를 담아요.
%*ENV
운영체제 환경 변수. 숫자 값은 동형(allomorph)으로 제공돼요.
$*REPO
이 변수는 설치/로드된 모듈에 대한 정보를 담아요.
$*INIT-INSTANT
$*INIT-INSTANT는 프로그램 시작 시각을 나타내는 Instant 객체예요. 특히 핵심 코드가 시작되는 시각이라, $*INIT-INSTANT의 값이 프로그램에서 실행한 INIT now나 BEGIN now보다 몇 밀리초 이를 수 있어요.
$*TZ
$*TZ는 시스템의 로컬 시간대에 대한 정보를 담은 객체를 담기 위한 동적 변수예요. GMT로부터의 초(數)로 수치화돼야 해요.
명시적으로 설정하지 않으면 추가 정보 없는 정수 값만 담는데, $*TZ에 처음 접근할 때 설정돼요. 프로세스가 지속되는 동안 발생하는 서머타임(일광 절약 시간) 변경은 그 경우 보이지 않아요.
$*CWD
현재 작업 디렉터리(Current Working Directory)를 담아요.
$*KERNEL
$*KERNEL은 Kernel 인스턴스를 담고, 그 .gist가 현재 실행 중인 커널이에요.
say $*KERNEL; # OUTPUT: «linux (4.4.92.31.default)»
$*DISTRO
Distro 타입의 이 객체는 현재 운영체제 배포판에 대한 정보를 담아요. 예:
say "Some sort of Windows" if $*DISTRO.is-win;
$*DISTRO.name은 운영체제에 따라 달라지는 값 모음을 가져요. 이 이름은 버전과 구현에 따라 달라지므로 프로그램에서 쓰기 전에 반드시 다시 확인하고 테스트해야 해요. 이 이름은 구현 정의이고 사양에 없어서 언제든 바뀔 수 있어요.
$*DISTRO의 gist는 say로 표시돼요:
say $*DISTRO; # OUTPUT: «debian (9.stretch)»
이것은 운영체제와 버전에 대한 추가 정보를 보여주지만, 사실 이 변수는 경로 구분자처럼 이식 가능한 프로그램을 만들 때 유용한 정보를 담아요:
say $*DISTRO.raku;
# OUTPUT: «Distro.new(release => "42.3", is-win => Bool::False,
# path-sep => ":", name => "opensuse",
# auth => "https://www.opensuse.org/", version => v42.3,
# signature => Blob, desc => "2018-12-13T08:50:59.213619+01:00")»
$*VM
이 변수는 코드를 실행하는 현재 가상 머신과, 앞서 언급한 VM의 내부 작동에 대한 추가 정보를 담아요.
say $*VM.precomp-ext, " ", $*VM.precomp-target; # OUTPUT: «moarvm mbc»
예를 들어 이 두 메서드는 사전 컴파일된 바이트코드 스크립트에 쓰인 확장자와 대상(target)을 보여줘요. 이것은 Moar 가상 머신에서 발견되는 값인데 버전과 구현에 따라 달라질 수 있어요. Java 같은 다른 VM은 다른 값을 보여줄 거예요. $*VM.config는 가상 머신을 만드는 데 쓰인 모든 설정 값을 포함해요. 예:
say $*VM.config<versionmajor>, ".", $*VM.config<versionminor>;
# OUTPUT: «2018.11»
이들은 가상 머신의 버전으로, 보통 인터프리터와 전체 Raku 환경에서 쓰는 것과 같아요.
$*RAKU
Raku 클래스의 이 객체는 Raku 언어의 현재 구현에 대한 정보를 담아요:
say $*RAKU.compiler.version; # OUTPUT: «v2020.01»
그 gist는 언어 이름 뒤에 컴파일러의 주 버전을 포함해요:
say $*RAKU; # OUTPUT: «Raku (6.d)»
Raku로 문자열화돼요:
$*RAKU.put; # OUTPUT: «Raku»
참고: Rakudo 2020.1 릴리스 이전에는 이 정보가 $*PERL 변수로만 가능했어요. Rakudo 2020.1 릴리스부터는 $*RAKU와 $*PERL 변수를 통해 모두 가능해요.
$*PERL
가까운 미래에는 $*RAKU와 같아요. 언젠가 deprecated될 거예요.
$*PID
현재 프로세스 식별자(운영체제 종속)를 설명하는 정수를 담은 객체예요.
$*PROGRAM-NAME
명령행에 입력된 현재 실행 파일의 경로를 담아요. -e 플래그로 raku를 호출했다면 -e를 담아요.
$*PROGRAM
실행 중인 Raku 프로그램의 위치를 IO::Path 객체 형태로 담아요.
&*EXIT
exit() 호출을 할 때 실행될 코드를 담는 Callable이에요. Raku가 다른 언어 런타임에 임베딩된 상황(예: Perl의 Inline::Perl6)에서 쓰이도록 의도된 거예요.
$*EXIT
$*EXIT는 보통 END 블록에서만 의미가 있어요. 현재 알려진 exit() 값을 담아요: 예외가 발생했으면 1, 예외가 없으면 0, exit(N)을 실행했으면 N이에요.
$*EXIT 지원은 Rakudo 컴파일러 2023.02 릴리스에서 추가됐어요.
$*EXCEPTION
$*EXCEPTION은 보통 END 블록에서만 의미가 있어요. 예외가 던져져 프로그램이 끝나면 인스턴스화된 Exception 객체를 담아요. 그렇지 않으면 Exception 타입 객체를 담아요.
$*EXCEPTION 지원은 Rakudo 컴파일러 2023.02 릴리스에서 추가됐어요.
$*EXECUTABLE
현재 실행 중인 raku 실행 파일의 IO::Path 절대 경로를 담아요.
$*EXECUTABLE-NAME
현재 실행 중인 Raku 실행 파일의 이름을 담아요. (예: raku-p, raku-m). raku 실행 파일이 PATH에 있다는 보장이 없으니 이것보다 $*EXECUTABLE을 선호하세요.
$*USAGE
sub MAIN과 sub USAGE 안에서 사용 가능한 MAIN sub의 시그니처로 생성된 기본 사용법 메시지를 담는 Str 타입 객체예요. 이 변수는 읽기 전용이에요.
sub MAIN($a, :$b, UInt :$ehehe) {
say $*USAGE; # OUTPUT: «Usage: my-script.raku [-a=<Int>] [-b=<Str>] [--<opts>=...]»
}
MAIN sub 안에서만 접근할 수 있어요.
$*USER
프로그램을 실행 중인 사용자에 대한 정보를 가진 Allomorph예요. 문자열로 취급하면 사용자 이름으로, 숫자로 취급하면 사용자 ID로 평가돼요.
$*GROUP
프로그램을 실행 중인 사용자의 주 그룹을 가진 Allomorph예요. 문자열로 취급해야 그룹 이름으로, 숫자로 취급해야 그룹 ID로 평가돼요.
$*HOMEDRIVE
Windows에서 프로그램을 실행 중인 사용자의 "홈 드라이브"에 대한 정보를 담아요. 다른 운영체제에서는 정의되지 않아요.
$*HOMEPATH
Windows에서 프로그램을 실행 중인 사용자 디렉터리로의 경로에 대한 정보를 담아요. 다른 운영체제에서는 정의되지 않아요.
$*HOME
프로그램을 실행 중인 사용자의 "홈 디렉터리"를 나타내는 IO::Path 객체를 담아요. 설정돼 있다면 %*ENV<HOME>을 사용해요.
Windows에서는 %*ENV<HOMEDRIVE> ~ %*ENV<HOMEPATH>를 사용해요. 홈 디렉터리를 결정할 수 없으면 Any가 돼요.
$*SPEC
프로그램이 실행되는 플랫폼에 적합한 IO::Spec 하위 클래스를 담아요. 운영체제를 위한 상위 레벨 클래스로, 예를 들어 Linux에서는 Unix를 반환해요(현재 구현에 쓰인 IO::Spec 클래스 형태로).
$*TMPDIR
.tmpdir IO::Spec::* 메서드로 결정된 "시스템 임시 디렉터리"를 나타내는 IO::Path 객체예요.
$*THREAD
현재 실행 중인 스레드를 나타내는 Thread 객체를 담아요.
$*SCHEDULER
현재 기본 스케줄러를 나타내는 ThreadPoolScheduler 객체예요.
기본적으로 이는 .hyper, .race 메서드와 그 스케줄러를 쓰는 Promise나 Supply 같은 다른 스레드 풀 클래스에 최대 64개 스레드를 부과해요. 물론 구현에 따라 다르고 바뀔 수 있어요. 스레드 최대 수를 바꾸려면 raku를 실행하기 전에 환경변수 RAKUDO_MAX_THREADS를 설정하거나, 쓰기 전에 기본값을 바꾼 스코프 복사본을 만들면 돼요:
my $*SCHEDULER = ThreadPoolScheduler.new( max_threads => 128 );
이 동작은 spec 테스트에서 검증되지 않았고 바뀔 수 있어요.
$*SAMPLER
시스템 상태의 스냅샷을 만드는 데 쓰는 현재 Telemetry::Sampler예요. Telemetry가 로드된 경우에만 사용 가능해요.
런타임 변수
이 변수들은 특정 함수의 동작에 영향을 주며, 어떤 경우엔 그 값이 런타임에 바뀔 수 있어요.
$*DEFAULT-READ-ELEMS
IO::Handle.read가 기본으로 읽는 바이트 수에 영향을 줘요. 기본 값은 65536이에요.
$*COLLATION
유니코드 조합(collation) 레벨을 구성하는 데 쓸 수 있는 Collation 객체예요.
$*RAT-OVERFLOW
Rakudo 컴파일러 2022.02 릴리스부터 사용 가능해요.
Rat을 쓰는 것 자체는 정밀도가 바닥나기 전까지 괜찮아요. $*RAT-OVERFLOW 동적 변수는 Rat이 정밀도를 넘칠 때 실행해야 할 동작을 지정해요. 기본적으로 Num으로 설정돼 있어 정밀도가 떨어지는 부동소수점으로 되돌아가요.
FatRat로 설정할 수도 있는데, 그러면 Rat이 정밀도를 넘치는 즉시 FatRat으로 자동 업그레이드돼요. Failure(실패), Exception(예외 던지기), CX::Warn(Num으로 내려갈 때 경고)도 지정할 수 있어요.
전역으로 FatRat으로 업그레이드하도록 활성화하려면:
INIT $*RAT-OVERFLOW = FatRat;
어휘 스코프에서만 활성화하려면:
my $*RAT-OVERFLOW = FatRat;
어떻게 동작할까요? $*RAT-OVERFLOW 변수는 Rat이 넘칠 때 UPGRADE-RAT 메서드가 호출될 클래스나 인스턴스를 담기로 되어 있어요. 그래서 UPGRADE-RAT 메서드를 가진 클래스를 만들어 자신만의 동작을 도입할 수 있어요.
그런 메서드는 정수 값 두 개를 받아야 해요: 분자 하나와 분모 하나. 예를 들어(완전히 진지한 건 아니지만) 결과 값이 정말 크면 Inf로, 정말 작으면 0으로 변환하는 거예요:
class ZeroOrInf {
method UPGRADE-RAT(Int $nu, Int $de) {
$nu > $de ?? Inf !! 0
}
}
$*TOLERANCE
=~= 연산자와, 그것에 의존하는 연산들이 두 값이 대략 같은지 결정하는 데 쓰는 변수예요. 기본값은 1e-15이에요.
명명 규칙 (Naming conventions)
코드가 하는 일을 바로 이해하려면 명명 규칙을 아는 게 도움이 돼요. 공식 목록은 아직(어쩌면 영원히) 없지만, 널리 따르는 몇 가지 규칙을 정리해요.
- 내장 라이브러리의 sub·method는 좋은 한 단어 이름을 찾을 수 있으면 한 단어로 쓰려고 해요. 이름이 두 단어 이상이면 "-"로 구분해요.
- 복합어는 한 단어로 취급해요. 그래서
substr,subbuf,deepmap(영어에서 "star fish"가 아니라 "starfish"라고 쓰는 것처럼). - 특별한 시점에 자동으로 호출되는 sub·method는 대문자로 써요.
MAINsub, 컨테이너 타입 구현을 위한AT-POS및 관련 메서드,BUILD와DESTROY를 포함해요. - 타입 이름은 네이티브 타입을 제외하고 카멜케이스예요. 네이티브 타입은 소문자인데, 더 컴팩트하게 저장돼서 이름도 더 작아 보인다로 기억하면 돼요.
- 내장 동적 변수와 컴파일 타임 변수는 항상 대문자예요. 예:
$*OUT,$?FILE. - MOP와 다른 내부 구현의 메서드는 여러 단어를 "_"로 구분해요. 예:
add_method.
[1] 아래 예시는 REPL에서는 제대로 실행되지 않고 동적 변수를 찾지 못한다는 오류가 나요. 파일에 복사·붙여넣기해 파일로 실행해 보세요.