컨텍스트와 컨텍스트화

컨텍스트와 컨텍스트화 (contexts)

"숫자를 문자열처럼 쓰는데 왜 자동으로 변환되지?" 이런 혼란을 겪은 적이 있나요? Raku에서는 값이 어떤 컨텍스트에서 사용되느냐에 따라 자동으로 타입이 조정돼요. 이 문서는 Raku의 컨텍스트라는 개념과, 각 컨텍스트가 값을 어떻게 해석하는지 설명해요. 컨텍스트를 이해하면 Raku 코드가 왜 그렇게 동작하는지 훨씬 명확하게 보여요.

출처: Raku Docs — Contexts and contextualizers

본문

컨텍스트는 컨테이너의 값을 해석해요. 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 컨텍스트를 강제할 수 있어요. Procsink 메서드로 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 해요. 어떤 경우엔 변환(예: StrNumeric)을 의미하고, 어떤 경우엔 단순히 해석(IntStrInt 또는 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 "";

이 컨텍스트는 ifwhile 같은 표현식에 나타나며, 해당 값에 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화할 수 있어요.

더 알아보기