Junction

Junction

여러 값을 "그중 하나라도", "전부", "정확히 하나" 같은 방식으로 묶어서 한 번에 비교하고 싶을 때가 있어요. Raku의 Junction은 바로 그런 복합 값을 다루는 타입이에요.

출처: Raku 공식 문서 — Junction

본문

Junction은 값이 없거나 여러 개인 값을 순서 없이 묶은 복합 값이에요. junction은 많은 연산에 대해 자동 스레딩(autothreading) 을 하는데, 이는 연산이 junction의 각 요소(또는 고유 상태, eigenstate 라고 해요)마다 수행되고, 그 결과가 모든 연산자 반환 값들의 junction이 된다는 뜻이에요.

Junction은 불리언 컨텍스트에서 하나의 값으로 붕괴(collapse)돼요. 조건문, 부정, 혹은 so? 접두 연산자로 Bool로 명시적 변환할 때 그렇죠. 이 붕괴의 의미는 junction 타입(이름)에 따라 달라져요. 타입은 all, any, one, none이 될 수 있어요.

type constructor operator True if ...
all all & no value evaluates to False
any any || at least one value evaluates to True
one one ^ exactly one value evaluates to True
none none | no value evaluates to True

표에서 보듯 junction을 만들 때는 Junction의 타입을 나타내는 명령 뒤에 어떤 객체든 붙이거나, 객체에 .all, .none, .one을 호출하면 돼요.

say so 3 == (1..30).one;         # OUTPUT: «True␤»
say so ("a" ^ "b" ^ "c") eq "a"; # OUTPUT: «True␤»

Junction은 아주 특별한 객체예요. Any 계층 밖에 있고, 다른 객체처럼 Mu의 서브클래스일 뿐이죠. 그 덕분에 대부분의 메서드에서 자동 스레딩이라는 기능을 쓸 수 있어요. 자동 스레딩은 junction이 Junction 타입을 받아들이지 않는 코드 객체의 매개 변수에 바인딩될 때 일어나요. 오류를 내는 대신, junction의 각 값에 대해 시그니처 바인딩이 반복되는 거예요.

예를 들어 보면,

my $j = 1|2;
if 3 == $j + 1 {
    say 'yes';
}

먼저 infix:<+> 연산자에 대해 자동 스레딩이 일어나 2|3이라는 junction을 만들어요. 다음 자동 스레딩 단계는 infix:<==>에 대해 일어나서 False|True를 만들죠. if 조건문은 불리언 컨텍스트에서 junction을 평가해 이걸 True로 붕괴시켜요. 그래서 이 코드는 yes\n을 출력합니다.

Junction의 타입은 자동 스레딩 후 결과 junction의 항목 개수에는 영향을 주지 않아요. 예를 들어 Hash 키 조회에 one junction을 써도 여전히 여러 항목을 가진 junction이 나와요. 타입이 영향을 주는 건 불리언 컨텍스트에서뿐이에요.

my %h = :42foo, :70bar;
say    %h{one <foo meow>}:exists; # OUTPUT: «one(True, False)␤»
say so %h{one <foo meow>}:exists; # OUTPUT: «True␤»
say    %h{one <foo  bar>}:exists; # OUTPUT: «one(True, True)␤»
say so %h{one <foo  bar>}:exists; # OUTPUT: «False␤»

컴파일러는 자동 스레딩(과 일반적인 Junction 동작)을 병렬화할 수 있는데, 필수는 아니라는 점을 기억해 두세요. 그래서 부작용(side effect)이 있는 코드에 junction을 자동 스레딩하는 건 보통 오류로 봅니다.

자동 스레딩은 그 함수가 평소 돌려주는 값들의 junction도 돌려준다는 뜻이에요.

(1..3).head( 2|3 ).say; # OUTPUT: «any((1 2), (1 2 3))␤»

.head는 리스트를 돌려주므로, 자동 스레딩된 버전은 리스트들의 junction을 돌려줘요.

'walking on sunshine'.contains( 'king'&'sun' ).say; # OUTPUT: «all(True, True)␤»

마찬가지로 .contains는 불리언을 돌려주니, 자동 스레딩된 버전은 불리언들의 junction을 돌려줘요. 일반적으로 타입 T의 인자를 받아 타입 TT를 돌려주는 모든 메서드와 루틴은 T의 junction도 받아들이고 TT의 junction을 돌려줍니다.

구현체들은 Junction을 단락(short-circuit)할 수 있어요. 예를 들어 아래 코드에서 a(), b(), c() 호출 중 하나 이상은 아예 실행되지 않을 수도 있어요. 이미 수행된 루틴 호출만으로 조건의 결과가 완전히 결정됐다면 말이죠(참인 값 하나만 반환돼도 junction 전체가 참임을 알 수 있어요).

if a() | b() | c() {
    say "At least one of the routines was called and returned a truthy value"
}

Junction은 불리언 컨텍스트에서 매처(matcher)로 쓰이게 되어 있어요. junction을 들여다보는(introspect) 것은 지원되지 않아요. junction을 들여다보고 싶으면 Set이나 관련 타입을 대신 쓰는 게 좋아요.

사용 예시들을 볼게요.

my @list = <1 2 "Great">;
@list.append(True).append(False);
my @bool_or_int = grep Bool|Int, @list;

sub is_prime(Int $x) returns Bool {
    # 'so' is for Boolean context
    so $x %% none(2..$x.sqrt);
}
my @primes_ending_in_1 = grep &is_prime & / 1$ /, 2..100;
say @primes_ending_in_1;        # OUTPUT: «[11 31 41 61 71]␤»

my @exclude = <~ .git>;
for dir(".") { say .Str if .Str.ends-with(none @exclude) }

all을 쓸 때는 빈 리스트를 만들 수 있는 인자에 주의를 기울여야 해요.

my @a = ();
say so all(@a) # True, because there are 0 Falses

"전부, 하지만 적어도 하나는"을 표현하려면 @a && all(@a)를 쓸 수 있어요.

my @a = ();
say so @a && all(@a);   # OUTPUT: «False␤»

부정 연산자는 자동 스레딩에서 특별하게 취급돼요. $a !op $b는 내부적으로 !($a op $b)로 다시 쓰여요. 바깥쪽 부정이 모든 junction을 붕괴시키므로, 반환 값은 항상 평범한 Bool이 됩니다.

my $word = 'yes';
my @negations = <no none never>;
if $word !eq any @negations {
    say '"yes" is not a negation';
}

이 특별 취급이 없다면, $word ne any @words 같은 표현은 한쪽에 trivial하지 않은 리스트가 있을 때 항상 True로 평가됐을 거예요.

이 목적을 위해 infix:<ne>infix:<eq>의 부정으로 간주돼요.

일반적으로는 긍정 비교 연산자와 부정된 junction을 쓰는 편이 더 읽기 좋아요.

my $word = 'yes';
my @negations = <no none never>;
if $word eq none @negations {
    say '"yes" is not a negation';
}

실패와 예외 (Failures and exceptions)

Junction의 관점에서 Failure는 다른 값과 똑같은 그저 하나의 값이에요.

my $j = +any "not a number", "42", "2.1";
my @list = gather for $j -> $e {
    take $e if $e.defined;
}
@list.say; # OUTPUT: «[42 2.1]␤»

위에서 Junction에 접두 + 연산자를 써서 그 안의 문자열들을 Numeric으로 강제 변환했어요. 숫자를 담지 않은 StrNumeric으로 강제 변환하면 Failure가 반환되기 때문에, junction의 요소 중 하나가 Failure가 됐죠. Failure는 사용되거나 sink되기 전에는 예외로 바뀌지 않아요. 그래서 정의됨(definedness)을 확인해 그렇게 되는 걸 피할 수 있어요. junction의 요소를 돌며 정의된 것만 리스트에 추가한 게 바로 그 작업이에요.

Failure를 값으로 사용하려고 하면 예외가 던져져요. 마치 그 Failure가 혼자 있고 junction의 일부가 아닌 것처럼요.

my $j = +any "not a number", "42", "2.1";
try say $j == 42;
$! and say "Got exception: $!.^name()";
# OUTPUT: «Got exception: X::Str::Numeric␤»

Junction의 값 중 어느 하나라도 계산될 때 예외가 던져지면, 마치 그 문제의 값이 junction과 함께가 아니라 혼자 계산된 것처럼 그 예외가 던져진다는 점을 기억하세요. 문제없는 값만 계산하고 예외는 무시할 수는 없어요.

sub calc ($_) { die when 13 }
my $j = any 1..42;
say try calc $j; # OUTPUT: «Nil␤»

위에서 예외를 일으키는 값은 하나뿐인데도 try 블록의 결과는 여전히 Nil이에요. 우회하려면 junction의 값들을 개별적으로 평가한 다음 그 결과로 junction을 다시 만드는 꼼수를 쓸 수 있어요.

sub calc ($_) { die when 13 }
my $j = any 1..42;
$j = any (gather $j».take).grep: {Nil !=== try calc $_};
say so $j == 42; # OUTPUT: «True␤»

스마트매칭 (Smartmatching)

~~의 오른쪽에 Junction을 쓰는 건 다른 연산자에 junction을 쓰는 것과 약간 다르게 동작해요.

이 예시를 볼게요.

say 25 == (25 | 42);    # OUTPUT: «any(True, False)␤» – Junction
say 25 ~~ (25 | 42);    # OUTPUT: «True␤»             – Bool

이유는 ==(과 대부분의 다른 연산자)는 자동 스레딩 대상이라 결과로 Junion을 얻지만, ~~는 오른쪽(이 경우 junction)에서 .ACCEPTS를 호출하고 결과가 Bool이 되기 때문이에요.

메서드

method new

multi method new(Junction: \values, Str :$type!)
multi method new(Junction: Str:D \type, \values)

이 생성자들은 junction을 정의하는 타입과 값의 집합으로부터 새 junction을 만들어요.

my $j = Junction.new(<Þor Oðinn Loki>, type => "all");
my $n = Junction.new( "one", 1..6 )

두 multi의 주요 차이는 Junction의 타입을 인자로 넘기는 방식이에요. 첫 번째 인자로 위치 인자로 넘기거나, type 이름 인자로 넘기는 거죠.

method defined

multi method defined(Junction:D:)

불리언 값 대신 정의됨(definedness)을 확인해요.

say ( 3 | Str).defined ;   # OUTPUT: «True␤»
say (one 3, Str).defined;  # OUTPUT: «True␤»
say (none 3, Str).defined; # OUTPUT: «False␤»

Failure도 정의되지 않은 것으로 간주돼요.

my $foo=Failure.new;
say (one 3, $foo).defined; # OUTPUT: «True␤»

6.d부터 이 메서드는 자동 스레딩됩니다.

method Bool

multi method Bool(Junction:D:)

Junction을 붕괴시키고, 타입과 담긴 값에 따라 단일 불리언 값을 돌려줘요. 각 요소가 Bool로 변환됩니다.

my $n = Junction.new( "one", 1..6 );
say $n.Bool;                         # OUTPUT: «False␤»

이 경우 모든 요소가 True로 변환되므로, 그중 하나만 참이라고 단정하는 건 거짓이에요.

my $n = Junction.new( "one", <0 1> );
say $n.Bool;                         # OUTPUT: «True␤»

이 경우에는 1 하나만 참이므로 Bool로의 변환이 True를 돌려줘요.

method Str

multi method Str(Junction:D:)

.Str 메서드를 요소들에 자동 스레딩하고 결과를 Junction으로 돌려줘요. .Str 메서드를 쓰는 출력 메서드(printput)는 Mu 타입을 받아들일 수 있음에도 junction을 자동 스레딩하도록 특별 취급돼요.

method iterator

multi method iterator(Junction:D:)

List로 변환된 Junction에 대한 이터레이터를 돌려줘요.

method gist

multi method gist(Junction:D:)

Junction을 붕괴시키고, junction의 타입과 구성 요소들의 gist로 이루어진 Str을 돌려줘요.

<a 42 c>.all.say; # OUTPUT: «all(a, 42, c)␤»

method raku

multi method raku(Junction:D:)

Junction을 붕괴시키고, 구성 요소들의 raku로 이루어진 Str을 돌려줘요. 이 문자열은 EVAL되면 동일한 구성 요소를 가진 동등한 Junction으로 평가됩니다.

<a 42 c>.all.raku.put; # OUTPUT: «all("a", IntStr.new(42, "42"), "c")␤»

infix ~

multi infix:<~>(Str:D $a, Junction:D $b)
multi infix:<~>(Junction:D $a, Str:D $b)
multi infix:<~>(Junction:D \a, Junction:D \b)

infix ~ 연결은 junction들을 하나로 병합하거나 junction과 문자열을 병합하는 데 쓸 수 있어요. 결과 junction은 마치 중첩 반복문으로 합친 것처럼 모든 요소가 병합된 형태가 돼요.

my $odd  = 1|3|5;
my $even = 2|4|6;

my $merged = $odd ~ $even;
say $merged; # OUTPUT: «any(12, 14, 16, 32, 34, 36, 52, 54, 56)␤»

say "Found 34!" if 34 == $merged; # OUTPUT: «Found 34!␤»
my $prefixed = "0" ~ $odd;
say "Found 03" if "03" == $prefixed; # OUTPUT: «Found 03!␤»

my $postfixed = $odd ~ "1";
say "Found 11" if 11 == $postfixed; # OUTPUT: «Found 11!␤»

반면에 한쪽 인자가 문자열인 ~ 버전은 junction의 모든 구성원에 그 문자열을 그냥 연결해서, 같은 개수의 요소를 가진 새 junction을 만들어요.

참고 자료 (See Also)