Raku의 ACCEPTS — 스마트매치의 심장

Raku의 ACCEPTS — 스마트매치의 심장

Raku에서 ~~ 연산자나 given/when을 쓰면 내부적으로 ACCEPTS 메서드가 호출돼요. 오른쪽 피연산자(매처)가 바로 이 메서드의 호출자입니다. 그런데 이 메서드는 타입마다 각자의 방식으로 동작하도록 수많은 타입·역할에 걸쳐 정의돼 있어요. 하나씩 살펴볼게요.

출처: Raku 공식 문서 — method ACCEPTS

Mu에서 — 기본 타입체크

Mu:U 멀티는 타입체크를 수행합니다. $other가 호출자(항상 타입 객체 또는 failure)에 부합하면 True를 돌려줘요.

multi method ACCEPTS(Mu:U: $other)
say 42 ~~ Mu;           # OUTPUT: «True␤»
say 42 ~~ Int;          # OUTPUT: «True␤»
say 42 ~~ Str;          # OUTPUT: «False␤»

주의할 점이 있어요. 정의된(defined) 호출자에 대한 멀티는 일부러 만들지 않습니다. 그렇게 해야 junction의 자동 스레딩(autothreading)이 가능해져요. 직접 분기할 후보가 없을 때 그 자동 스레딩이 폴백 메커니즘으로 동작하거든요.

Any에서 — 객체 정체성 확인

Any:D 호출자는 $other === self, 즉 객체 정체성이 같은지 확인합니다. 다만 많은 내장 타입이 더 구체적인 비교를 위해 이 메서드를 오버라이드하기 때문에, 이 기본 동작은 폴백처럼 이해하면 돼요.

multi method ACCEPTS(Any:D: Mu $other)

사용 형태는 다음과 같아요.

EXPR.ACCEPTS(EXPR);

Whatever에서 — 항상 참 vs 타입체크

Whatever는 인스턴스(defined)와 타입 객체(undefined)일 때 행동이 달라져요.

multi method ACCEPTS(Whatever:D: Mu $other)
multi method ACCEPTS(Whatever:U: Mu $other)

인스턴스면 항상 True를 반환하고, 타입 객체면 타입체크를 수행합니다.

say 42 ~~ (*);       # OUTPUT: «True␤»
say 42 ~~ Whatever;  # OUTPUT: «False␤»

Regex에서 — 정규식 매칭

인자를 받아 그 대상에 정규식을 매칭합니다. 인자가 Positional이면 리스트 항목 중 첫 번째 성공 매치를, Associative면 키 중 첫 번째 성공 매치를 돌려줘요. 그 외에는 인자를 Str로 해석해 매칭합니다.

multi method ACCEPTS(Regex:D: Mu --> Match:D)
multi method ACCEPTS(Regex:D: @)
multi method ACCEPTS(Regex:D: %)

PositionalAssociative 매치의 경우, 실패하면 Nil을 반환합니다.

Str에서 — 문자열 동일성

문자열이 $other와 같으면 True를 반환합니다.

multi method ACCEPTS(Str:D: $other)

enum Bool에서 — 참·거짓 그대로

스마트매치 비교에 쓰이며, 오른쪽이 True면 항상 True를, False면 항상 False를 반환합니다. 특히 ACCEPTS는 항상 그 자신, 즉 스마트매치의 오른쪽(호출자)을 돌려줘요.

method ACCEPTS(Bool:D: --> Bool:D)
my $b = Bool.new( True );
# when True on the right side returns
# always True
True  ~~ $b;     # True
False ~~ $b;     # True

$b = Bool.new( False );
# when False on the right side
# returns always False
False ~~ $b;     # False
True ~~ $b;      # False

role Numeric에서 — 수치적 동등

$otherNumeric으로 강제 변환할 수 있고, 호출자와 수치적으로 같은 값이면 True를 반환합니다. 둘 다 NaN으로 평가되는 경우도 True예요.

multi method ACCEPTS(Numeric:D: $other)

Allomorph에서 — 숫자 값/문자열 값에 따라

Allomorph(숫자와 문자열을 둘 다 갖는 타입)는 a 파라미터의 타입에 따라 다르게 동작합니다. aNumeric(다른 allomorph 포함)이면 호출자의 Numeric 값이 aACCEPTS하는지, Str이면 호출자의 Str 값이 aACCEPTS하는지, 그 외라면 호출자의 Numeric·Str둘 다 aACCEPTS하는지 확인해요.

multi method ACCEPTS(Allomorph:D: Any:D \a)
say "5.0" ~~ <5>; # OUTPUT: «False␤»
say 5.0   ~~ <5>; # OUTPUT: «True␤»
say <5.0> ~~ <5>; # OUTPUT: «True␤»

문자열 "5.0"은 숫자 값과 비교할 때 False이고, 숫자 5.0과 allomorph <5.0>True로 나오는 차이를 볼 수 있어요.

List에서 — 두 Iterable의 내용 비교

$topicIterable이면 두 Iterable의 내용이 일치하는지에 따라 True/False를 돌려줍니다. 호출자에 Whatever(*) 요소가 있으면 $topic의 같은 위치에 있는 무엇이든 매칭되고, HyperWhatever(**)는 개수에 상관없이(심지어 0개도) 아무 요소나 매칭해요.

multi method ACCEPTS(List:D: $topic)
say (1, 2, 3)       ~~ (1,  *, 3);  # OUTPUT: «True␤»
say (1, 2, 3)       ~~ (9,  *, 5);  # OUTPUT: «False␤»
say (1, 2, 3)       ~~ (   **, 3);  # OUTPUT: «True␤»
say (1, 2, 3)       ~~ (   **, 5);  # OUTPUT: «False␤»
say (1, 3)          ~~ (1, **, 3); # OUTPUT: «True␤»
say (1, 2, 4, 5, 3) ~~ (1, **, 3); # OUTPUT: «True␤»
say (1, 2, 4, 5, 6) ~~ (1, **, 5); # OUTPUT: «False␤»
say (1, 2, 4, 5, 6) ~~ (   **   ); # OUTPUT: «True␤»
say ()              ~~ (   **   ); # OUTPUT: «True␤»

추가 규칙이 있어요. 호출자나 $topic 중 하나가 lazy Iterable이면 False를 반환하는데, 단 $topic이 호출자와 같은 객체라면 True를 돌려줍니다. 그리고 $topicIterable이 아닐 경우, 호출자에 요소가 없거나 첫 요소가 Match 객체라면 호출자(자기 자신)를, 그 외에는 False를 반환합니다. 이 동작 덕분에 m:g// 스마트매치가 가능해져요.

Map에서 — 키 존재 여부

오른쪽이 Map일 때 스마트매칭에 쓰입니다. $topic의 형태에 따라 달라져요.

multi method ACCEPTS(Map:D: Positional $topic)
multi method ACCEPTS(Map:D: Cool:D     $topic)
multi method ACCEPTS(Map:D: Regex      $topic)
multi method ACCEPTS(Map:D: Any        $topic)
  • $topic이 리스트 형태(Positional)면 리스트 요소 중 하나라도 Map의 키로 존재하면 True.
  • $topicCool(문자열·정수 등)이면 그 값이 키로 존재하면 True.
  • $topic이 정규식이면 키 중 하나라도 그 정규식에 매칭되면 True.
  • 폴백으로는 $topic을 리스트로 강제 변환해 Positional 동작을 적용합니다.

Range에서 — 범위 포함 관계

Range가 다른 Range를 **포함(겹침)**하는지 나타냅니다.

multi method ACCEPTS(Range:D: Mu \topic)
multi method ACCEPTS(Range:D: Range \topic)
multi method ACCEPTS(Range:D: Cool:D \got)
multi method ACCEPTS(Range:D: Complex:D \got)
my $p = Range.new( 3, 5  );
my $r = Range.new( 1, 10 );

say $p.ACCEPTS( $r );    # OUTPUT: «False␤»
say $r.ACCEPTS( $p );    # OUTPUT: «True␤»
say $r ~~ $p;            # OUTPUT: «False␤»  (same as $p.ACCEPTS( $r ))
say $p ~~ $r;            # OUTPUT: «True␤»   (same as $r.ACCEPTS( $p ))

무한 Range는 어떤 Range든 항상 포함합니다.

say 1..10 ~~ -∞..∞;    # OUTPUT: «True␤»
say 1..10 ~~ -∞^..^∞;  # OUTPUT: «True␤»

마찬가지로, 열린 경계(open boundary)를 가진 Range도 다른 범위를 흔히 포함해요.

say 1..2 ~~ *..10;  # OUTPUT: «True␤»
say 2..5 ~~ 1..*;   # OUTPUT: «True␤»

숫자 범위뿐 아니라 문자열 기반 범위도 가능합니다.

say 'a'..'j' ~~ 'b'..'c';  # OUTPUT: «False␤»
say 'b'..'c' ~~ 'a'..'j';  # OUTPUT: «True␤»
say 'raku' ~~ -∞^..^∞;     # OUTPUT: «True␤»
say 'raku' ~~ -∞..∞;       # OUTPUT: «True␤»
say 'raku' ~~ 1..*;        # OUTPUT: «True␤»

정수 RangeCool(문자열)과 스마트매치할 때는 before·after 연산자를 이용해 그 값이 범위와 겹치는지 확인합니다.

say 1..10 ~~ '5';  # OUTPUT: «False␤»
say '5' before 1;  # OUTPUT: «False␤»
say '5' after 10;  # OUTPUT: «True␤»
say '5' ~~ *..10;  # OUTPUT: «False␤»

위 예시에서 문자열 '5'가 정수 10보다 뒤(after)에 있으므로, 범위와 겹치지 않아 False가 나와요. Mu 인스턴스(일반적 인스턴스)와 매칭할 때는 cmp 연산자를 사용합니다.

role Setty에서 — 집합 원소 동일성

$otherself똑같은 원소들만 갖고 있으면 True를 반환합니다.

method ACCEPTS($other)

role Baggy에서 — 원소와 가중치까지

오른쪽이 Baggy일 때 스마트매칭에 쓰입니다.

method ACCEPTS($other --> Bool:D)

오른쪽이 타입 객체 Baggy$otherBaggydoes하면 True, 아니면 False를 돌려줘요. 오른쪽이 Baggy 객체면, $other가 호출자와 같은 원소를 같은 가중치로 가질 때만 True입니다.

my $breakfast = bag <eggs bacon>;
say $breakfast ~~ Baggy;                            # OUTPUT: «True␤»
say $breakfast.does(Baggy);                         # OUTPUT: «True␤»

my $second-breakfast = (eggs => 1, bacon => 1).Mix;
say $breakfast ~~ $second-breakfast;                # OUTPUT: «True␤»

my $third-breakfast = (eggs => 1, bacon => 2).Bag;
say $second-breakfast ~~ $third-breakfast;          # OUTPUT: «False␤»

bacon의 가중치만 다른 $third-breakfastFalse가 나오는 걸로, 가중치까지 비교한다는 걸 확인할 수 있어요.

Code에서 — 코드 호출

보통 코드 객체를 호출하면서 $topic을 인자로 넘깁니다. 그런데 인자를 받지 않는 코드 객체에 호출되면 인자 없이 호출하고 $topic은 버려요. 호출 결과를 돌려줍니다.

multi method ACCEPTS(Code:D: Mu $topic)

Pair에서 — 키/값 패턴 매칭

%topicAssociative면 호출자의 키로 그 안의 값을 찾아, 호출자의 값이 그 값을 ACCEPTS하는지 확인합니다.

multi method ACCEPTS(Pair:D $: %topic)
multi method ACCEPTS(Pair:D $: Pair:D $topic)
multi method ACCEPTS(Pair:D $: Mu $topic)
say %(:42a) ~~ :42a; # OUTPUT: «True␤»
say %(:42a) ~~ :10a; # OUTPUT: «False␤»

$topic이 다른 Pair면 호출자의 키·값이 각각 $topic의 키·값을 ACCEPTS하는지 확인합니다.

say :42a ~~ :42a; # OUTPUT: «True␤»
say :42z ~~ :42a; # OUTPUT: «False␤»
say :10a ~~ :42a; # OUTPUT: «False␤»

$topic이 그 외의 값이면, 호출자 Pair키를 메서드 이름으로 간주해 $topic에서 그 메서드를 호출하고, 그 Bool 결과를 호출자 PairBool 값과 비교합니다. 예를 들어 소수 판정을 스마트매치로 확인할 수 있어요.

say 3 ~~ :is-prime;             # OUTPUT: «True␤»
say 3 ~~  is-prime => 'truthy'; # OUTPUT: «True␤»
say 4 ~~ :is-prime;             # OUTPUT: «False␤»

이 형태는 같은 객체의 여러 메서드 Bool 값을 확장해서 확인할 때도 쓸 수 있어요. IO::Path 같은 경우 junction을 이용하면 되죠.

say "foo" .IO ~~ :f & :rw; # OUTPUT: «False␤»
say "/tmp".IO ~~ :!f;      # OUTPUT: «True␤»
say "."   .IO ~~ :f | :d;  # OUTPUT: «True␤»

Signature에서 — 시그니처 수용 여부

$topicSignature면, $topic이 허용하는 것을 호출자도 모두 허용하면 True를 반환합니다.

multi method ACCEPTS(Signature:D: Signature $topic)
multi method ACCEPTS(Signature:D: Capture $topic)
multi method ACCEPTS(Signature:D: Mu \topic)
:($a, $b) ~~ :($foo, $bar, $baz?);   # OUTPUT: «True»
:(Int $n) ~~ :(Str);                 # OUTPUT: «False»

$topicCapture면 그것이 호출자에 바인딩될 수 있는지, 다시 말해 호출자의 시그니처를 가진 함수가 $topic으로 호출될 수 있는지를 확인합니다.

\(1, 2, :foo) ~~ :($a, $b, :foo($bar)); # OUTPUT: «True»
\(1, :bar)    ~~ :($a);                 # OUTPUT: «False»

마지막으로 Mu \topic 후보는 topicCapture로 변환해 Capture $topic과 같은 의미론을 따릅니다.

<a b c d>  ~~ :(Int $a);      # OUTPUT: «False»
42         ~~ :(Int);         # OUTPUT: «False» (Int.Capture throws)
set(<a b>) ~~ :(:$a, :$b);    # OUTPUT: «True»

where 절은 들여다볼(introspect) 수 없기 때문에, 두 시그니처가 같은 종류의 where 제약 파라미터를 ACCEPTS하는지 이 메서드는 판단할 수 없어요. 그런 비교는 False를 반환합니다. 리터럴이 포함된 시그니처도 마찬가지인데, 리터럴은 결국 where 제약의 설탕일 뿐이거든요.

say :(42) ~~ :($ where 42)    # OUTPUT: «False␤»

IO::Path에서 — 경로 해석 일치

인자를 필요하면 IO::Path로 강제 변환한 뒤, 두 경로의 .absolute 메서드 결과가 같은 문자열이면 True를 반환합니다. 다만 주의할 점이 있어요. 겉보기엔 같은 자원을 가리키는 두 경로라도, 서로 다르게 만들어져 완전히 해석(resolve)되지 않았다면 스마트매치가 True가 아닐 수 있습니다.

multi method ACCEPTS(IO::Path:D: Cool:D $other --> Bool:D)
say "foo/../bar".IO ~~ "bar".IO # False

이유는 두 경로가 완전히 해석됐을 때 서로 다른 자원을 가리킬 수 있기 때문이에요(예를 들어 foo가 심볼릭 링크인 경우). 같은 자원을 가리키는지 확인하려면 스마트매치 전에 경로를 해석하면 됩니다.

say "foo/../bar".IO.resolve(:completely) ~~ "bar".IO.resolve(:completely) # True

정리하면, ACCEPTS는 Raku 스마트매치의 중심 역할을 하는 메서드로, 매처의 타입에 따라 타입체크·정규식 매칭·범위 포함·시그니처 수용·가중치 비교까지 다양한 의미를 담당해요. 매처가 무엇이냐에 따라 어느 멀티가 선택되는지만 알면, ~~가 어떤 결과를 낼지 자연스럽게 예상할 수 있을 거예요.