컨텍스트와 컨텍스트화
컨텍스트와 컨텍스트화 (contexts)
"숫자를 문자열처럼 쓰는데 왜 자동으로 변환되지?" 이런 혼란을 겪은 적이 있나요? Raku에서는 값이 어떤 컨텍스트에서 사용되느냐에 따라 자동으로 타입이 조정돼요. 이 문서는 Raku의 컨텍스트라는 개념과, 각 컨텍스트가 값을 어떻게 해석하는지 설명해요. 컨텍스트를 이해하면 Raku 코드가 왜 그렇게 동작하는지 훨씬 명확하게 보여요.
본문
컨텍스트는 컨테이너의 값을 해석해요. Raku에서는 활성 컨텍스트를 사용해 컨테이너의 값을 특정 타입·클래스로 강제 변환(coerce)하거나, 그 값으로 무엇을 할지 결정해요. 보통 컨텍스트가 객체를 받으면, 필요하다면 특정 메서드를 암시적으로 호출해 그 객체를 강제 변환해요.
Sink
sink 컨텍스트는 다른 언어에서 void 컨텍스트라고 부르는 것과 같아요. 어떤 코드(term, operation, block)의 결과나 반환값으로 아무것도 하지 않는 컨텍스트예요. 일반적으로 이 컨텍스트가 값을 소비하면서 그 값을 무시하면 값을 버리는 것이므로 경고나 오류가 발생해요. sink의 연상은 무언가를 없애는 것과 관련돼요. 싱크대 배수구로 물이 빠지듯, 배가 침몰하듯, 히트싱크가 열을 없애듯이요.
my $sub = -> $a { $a² };
$sub; # OUTPUT: «WARNINGS:Useless use of $sub in sink context (line 1)»
Iterator에는 sink-all 메서드로 sink 컨텍스트를 강제할 수 있어요. Proc도 sink 메서드로 sink될 수 있고, 그러면 예외를 던지고 아무것도 반환하지 않게 돼요.
대부분의 블록은 sink 컨텍스트에서 평가되면 경고해요. 하지만 gather/take 블록은 명시적으로 sink 컨텍스트에서 평가되고, 값은 take로 명시적으로 반환돼요.
my @results = gather for 1..1 { ^10 .map: *.take };
say @results; # OUTPUT: «[0 1 2 3 4 5 6 7 8 9]»
이 예에서 for가 sink 컨텍스트에서 실행되고, 그 안의 map도 마찬가지예요. 결과는 gather/take를 통해 루프에서 명시적으로 가지게 돼요.
sink 컨텍스트에서 객체는 sink 메서드가 있다면 그것을 호출해요.
sub foo {
return [<a b c>] does role {
method sink { say "sink called" }
}
}
foo; # OUTPUT: «sink called»
Number
이 컨텍스트와, 위의 sink를 제외한 다른 모든 컨텍스트는 conversion이나 interpretation 컨텍스트예요. 타입이 없거나 있는 변수를 받아 연산을 수행하는 데 필요한 타입으로 duck-typings 해요. 어떤 경우엔 변환(예: Str → Numeric)을 의미하고, 어떤 경우엔 단순히 해석(IntStr을 Int 또는 Str로 해석)을 의미해요.
Number 컨텍스트는 변수에 숫자 연산을 적용해야 할 때마다 호출돼요.
my $stringone = "1 ";
my $stringthree = "3 ";
say $stringone + $stringthree; # OUTPUT: «4»
위 코드에서 문자열은 숫자가 몇 개뿐이고 다른 문자가 없는 한 숫자 컨텍스트로 해석돼요. 다만 앞뒤 공백은 얼마든지 있을 수 있어요.
+나 - 같은 산술 연산자를 쓰면 숫자 컨텍스트를 강제할 수 있어요. 그 컨텍스트에서 Numeric 메서드가 있으면 호출되고, 반환된 값이 객체의 숫자값으로 쓰여요.
my $t = True;
my $f = False;
say $t + $f; # OUTPUT: «1»
say $t.Numeric; # OUTPUT: «1»
say $f.Numeric; # OUTPUT: «0»
my $list= <a b c>;
say True + $list; # OUTPUT: «4»
say +" \n "; # OUTPUT: «0»
마지막 문장에서 보듯 공백은 얼마든지 0으로 변환돼요. 목록 같은 것의 경우 숫자값은 일반적으로 .elems와 같아요. Thread.numeric 같은 경우엔 고유한 스레드 식별자를 반환해요.
String
문자열 컨텍스트에서 값은 문자열로 조작할 수 있어요. 이 컨텍스트는 예를 들어 표준 출력으로 출력할 수 있도록 문자열이 아닌 값을 강제 변환할 때 사용돼요.
put $very-complicated-and-hairy-object; # OUTPUT: something meaningful
또는 정규식에 스마트매치할 때요.
put 333444777 ~~ /(3+)/; # OUTPUT: «333»
일반적으로 변수를 컨텍스트화하려면 Str 루틴이 호출돼요. 이 메서드는 Mu에서 상속되므로 항상 존재하지만, 항상 동작하리란 보장은 없어요. 일부 코어 클래스에서는 경고를 내요.
~는 (단항) 문자열 컨텍스트화자예요. 연산자로는 문자열을 연결하고, 접두 연산자로는 문자열 컨텍스트 연산자가 돼요.
my @array = [ [1,2,3], [4,5,6]];
say ~@array; # OUTPUT: «1 2 3 4 5 6»
이런 일은 reduction 컨텍스트에서 [~]를 목록에 적용할 때도 일어나요.
say [~] [ 3, 5+6i, Set(<a b c>), [1,2,3] ]; # OUTPUT: «35+6ic a b1 2 3»
그런 의미에서 빈 목록이나 다른 컨테이너는 빈 문자열로 문자열화돼요.
say [~] [] ; # OUTPUT: «»
~가 버퍼 연결 연산자로도 작동하기 때문에, 문자열 컨텍스트에서 빈 버퍼 하나가 문자열처럼 동작해 오류를 내므로 각 요소가 비어 있지 않은지 확인해야 해요.
say [~] Buf.new(0x3,0x33), Buf.new(0x2,0x22);
# OUTPUT: «Buf:0x<03 33 02 22>»
하지만,
my $non-empty = Buf.new(0x3, 0x33);
my $empty = [];
my $non-empty-also = Buf.new(0x2,0x22);
say [~] $non-empty, $empty, $non-empty-also;
# OUTPUT: «Cannot use a Buf as a string, but you called the Stringy method on it
~가 이 목록의 두 번째 요소를 문자열 컨텍스트에 넣으므로, ~는 문자열에 적용되는 두 번째 형태를 쓰게 되어 위 오류가 나요. 연결하는 모든 것이 버퍼인지 확인하기만 하면 이 문제를 피할 수 있어요.
my $non-empty = Buf.new(0x3, 0x33);
my $empty = Buf.new();
my $non-empty-also = Buf.new(0x2,0x22);
say [~] $non-empty, $empty, $non-empty-also; # OUTPUT: «Buf:0x<03 33 02 22>»
일반적으로 컨텍스트는 컨텍스트화자(contextualizer)를 호출해 변수를 특정 타입으로 강제 변환해요. mixin의 경우, 컨텍스트 클래스가 mixin되면 그렇게 동작해요.
my $described-number = 1i but 'Unity in complex plane';
put $described-number; # OUTPUT: «Unity in complex plane»
but는 mixin을 만드는데, 이 mixin은 복소수에 Str 메서드를 부여해요. put은 그것을 문자열로 컨텍스트화해요. 즉 문자열 컨텍스트화자인 Str을 호출해 위 결과를 보여줘요.
Boolean
이 컨텍스트는 변수를 True 또는 False로 해석하게 강제해요.
say "Hey" if 7; # OUTPUT: «Hey»
say "Ahoy" if "";
이 컨텍스트는 if나 while 같은 표현식에 나타나며, 해당 값에 so를 호출하는 것과 같아요.
say "Hey" if 7.so; # OUTPUT: «Hey»
say "Ahoy" if not set().so; # OUTPUT: «Ahoy»
일반적으로 0이 아니고 비어 있지 않은 것은 True로, 0이거나 빈 것은 False로 변환돼요. 하지만 .so는 우리가 원하는 어떤 Boolean 값이든 반환하도록 정의될 수 있어서, 이건 그저 경험 법칙일 뿐이에요.
? Boolean 컨텍스트 연산자와 ! 부정 Boolean 컨텍스트 연산자가 객체에 Boolean 컨텍스트를 강제해요.
say ? 0i; # OUTPUT: «False»
say ! :true; # OUTPUT: «False»
List
실제로는 여러 가지 서로 다른 list 컨텍스트가 있어요. 그건 그 페이지에서 더 잘 설명돼요. 일반적으로 list 컨텍스트화자는 쉼표 ,예요.
say (3,).^name; # OUTPUT: «List»
그 경우 호출되는 메서드도 .list예요.
Any.list.^name; # OUTPUT: «List»
say 3.list.^name; # OUTPUT: «List»
say (^3).list; # OUTPUT: «(0 1 2)»
Item 컨텍스트
Item(또는 스칼라) 컨텍스트는 복잡한 데이터 덩어리를 단일 항목처럼 취급해요. 스칼라 변수에 할당하려고 할 때 강제돼요.
my $scalar-context = <1 2 3>;
say "→ $_" for $scalar-context; # OUTPUT: «→ 1 2 3»
$ 연산자로 유도할 수 있어요. 이 연산자는 메서드·루틴으로 item을 호출하는 컨텍스트화자 연산자 역할을 해요.
.say for $(1,2,3); # OUTPUT: «(1 2 3)»
.say for (1,2,3).item; # OUTPUT: «(1 2 3)»
.say for item( 1..3 ); # OUTPUT: «1..3»
Item화는 목록 컨텍스트에서의 동작에만 영향을 줘요. 그러나 여전히 Positional role이나 다른 role들을 유지해요.
$(1,2,3).elems.say; # OUTPUT: «3»
say (1,2,3).item[2]; # OUTPUT: «3»
say $( key => 'value')<key>; # OUTPUT: «value»
Item화는 데이터 구조의 값을 컨테이너화해서, 예를 들어 주변 목록이나 데이터 구조로 평탄화(flatten)되지 않게 해요.
.say for (1, $(2,3), 4).flat; # OUTPUT: «1(2 3)4»
say (1, $<a b>, 2).elems; # OUTPUT: «3»
Item화 연산자는 객체의 .item 메서드를 호출해요. 이것은 서브루틴으로도 호출할 수 있어요. Mu에서 상속된 메서드`이므로, 어떤 클래스의 객체든 item화할 수 있어요.
더 알아보기
- List 컨텍스트 — 여러 목록 컨텍스트
- 연산자 — reduction 연산자,
?·!접두 연산자 - Sink 컨텍스트 —
sink-all메서드 - 주제 변수와 twigil —
$_와 특수 변수