함수
함수 (Functions)
코드를 여러 번 재사용하고 싶을 때 Raku에서 쓸 수 있는 방법 중 하나가 루틴(routine)이에요. 루틴은 여러 형태가 있는데, 클래스와 역할에 속해 객체와 연결되는 Method가 가장 대표적이고, 객체와 무관하게 호출할 수 있는 함수(줄여서 서브루틴 또는 Sub이라고도 불러요)도 있어요.
본문
서브루틴은 기본적으로 어휘 스코프(my)를 가지며, 서브루틴 호출은 보통 컴파일 시간에 결정돼요.
서브루틴은 파라미터 목록이라고도 부르는 Signature를 가질 수 있어요. 시그니처는 어떤 인자를 기대하는지(있다면) 지정해요. 인자의 개수와 타입, 그리고 반환값을 모두 지정하거나 열어 둘 수 있어요.
서브루틴에 대한 내성(introspection)은 Routine으로 제공돼요.
함수 정의·생성·사용
서브루틴 (Subroutines)
서브루틴을 만드는 기본 방법은 sub 선언자를 쓴 뒤 선택적으로 식별자를 붙이는 거예요.
sub my-func { say "Look ma, no args!" }
my-func;
sub 선언자는 Sub 타입의 값을 반환하는데, 이 값은 어떤 컨테이너에든 저장할 수 있어요.
my &c = sub { say "Look ma, no name!" }
c; # OUTPUT: «Look ma, no name!»
my Any:D $f = sub { say 'Still nameless...' }
$f(); # OUTPUT: «Still nameless...»
my Code \a = sub { say ‚raw containers don't implement postcircumfix:<( )>‘ };
a.(); # OUTPUT: «raw containers don't implement postcircumfix:<( )>»
sub 선언자는 현재 스코프에 새 이름을 컴파일 시간에 선언해요. 그래서 간접(indirection) 호출은 컴파일 시간에 해결돼야 해요.
constant aname = 'foo';
sub ::(aname) { say 'oi‽' };
foo;
이건 Raku에 매크로가 추가되면 더 유용해질 거예요.
서브루틴이 인자를 받게 하려면, 서브루틴 이름과 본문 사이에 괄호로 Signature를 넣어요.
sub exclaim ($phrase) {
say $phrase ~ "!!!!"
}
exclaim "Howdy, World";
기본적으로 서브루틴은 어휘 스코프로 해요. 즉 sub foo {...}는 my sub foo {...}와 같고 현재 스코프 안에서만 정의돼요.
sub escape($str) {
# Puts a slash before non-alphanumeric characters
S:g[<-alpha -digit>] = "\\$/" given $str
}
say escape 'foo#bar?'; # OUTPUT: «foo\#bar\?»
{
sub escape($str) {
# Writes each non-alphanumeric character in its hexadecimal escape
S:g[<-alpha -digit>] = "\\x[{ $/.ord.base(16) }]" given $str
}
say escape 'foo#bar?' # OUTPUT: «foo\x[23]bar\x[3F]»
}
# Back to original escape function
say escape 'foo#bar?'; # OUTPUT: «foo\#bar\?»
서브루틴은 반드시 이름을 가질 필요는 없어요. 이름이 없으면 익명(anonymous) 서브루틴이라고 불러요.
say sub ($a, $b) { $a ** 2 + $b ** 2 }(3, 4) # OUTPUT: «25»
하지만 이 경우에는 더 간결한 Block 문법을 쓰는 게 좋은 경우가 많아요. 서브루틴과 블록은 위 예제처럼 제자리에서 호출할 수 있어요.
say -> $a, $b { $a ** 2 + $b ** 2 }(3, 4) # OUTPUT: «25»
심지어 이렇게도 가능해요.
say { $^a ** 2 + $^b ** 2 }(3, 4) # OUTPUT: «25»
블록과 람다 (Blocks and lambdas)
{ $_ + 42 }, -> $a, $b { $a ** $b }, { $^text.indent($:spaces) } 같은 걸 볼 때마다 그것이 Block 문법이에요. ->도 블록의 일부로 봐요. if, for, while 같은 문장 뒤에는 이런 종류의 블록이 따라와요.
for 1, 2, 3, 4 -> $a, $b {
say $a ~ $b;
}
# OUTPUT: «1234»
블록은 그 자체로 익명 코드 블록으로도 쓸 수 있어요.
say { $^a ** 2 + $^b ** 2}(3, 4) # OUTPUT: «25»
이건 if 같은 문장이 토픽 변수를 정의하지 않음에도 사실 정의할 수 있다는 걸 뜻해요.
my $foo = 33;
if $foo ** 33 -> $a {
say "$a is not null"; #
} # OUTPUT: «129110040087761027839616029934664535539337183380513 is not null»
블록 문법에 대한 자세한 내용은 Block 타입 문서를 참고하세요.
시그니처 (Signatures)
함수가 받는 파라미터는 그 시그니처에 설명돼 있어요.
sub format(Str $s) { ... }
-> $a, $b { ... }
시그니처 문법과 사용에 대한 자세한 내용은 Signature 클래스 문서에서 찾을 수 있어요.
자동 시그니처 (Automatic signatures)
시그니처가 제공되지 않았는데 함수 본문에서 자동 변수 @_나 %_가 사용되면, *@_ 또는 *%_를 가진 시그니처가 생성돼요. 두 자동 변수를 동시에 쓰는 것도 가능해요.
sub s { say @_, %_ };
say &s.signature # OUTPUT: «(*@_, *%_)»
인자 (Arguments)
인자는 쉼표로 구분된 목록으로 제공돼요. 중첩 호출을 명확히 하려면 괄호를 사용해요.
sub f(&c){ c() * 2 }; # call the function reference c with empty parameter list
sub g($p){ $p - 2 };
say(g(42), 45); # pass only 42 to g()
함수를 호출할 때 위치 인자(positional argument)는 함수 시그니처와 같은 순서로 제공돼야 해요. 이름 인자(named argument)는 어떤 순서로든 제공할 수 있지만, 위치 인자 뒤에 두는 게 보기 좋아요. 함수 호출의 인자 목록 안에서는 몇 가지 특별한 문법을 지원해요.
sub f(|c){};
f :named(35); # A named argument (in "adverb" form)
f named => 35; # Also a named argument
f :35named; # A named argument using abbreviated adverb form
f 'named' => 35; # Not a named argument, a Pair in a positional argument
f 'hi', :1x, :2y; # Positional and named arguments
my \c = <a b c>.Capture;
f |c; # Merge the contents of Capture $c as if they were supplied
함수에 전달된 인자는 개념적으로 먼저 Capture 컨테이너에 모여요. 이 컨테이너의 문법과 사용에 대한 자세한 내용은 Capture 문서에서 찾을 수 있어요.
이름 인자를 사용할 때는 일반 목록의 "pair-chaining" 덕분에 이름 인자 사이의 쉼표를 생략할 수 있어요.
sub f(|c){};
f :dest</tmp/foo> :src</tmp/bar> :lines(512);
f :32x :50y :110z; # This flavor of "adverb" works, too
f :a:b:c; # The spaces are also optional.
위치 인자도 함께 전달한다면, 함수 이름 바로 뒤에 괄호로 감싸서 전달하거나, 축약 adverb 형태로 이름 인자를 연결할 때 마지막 위치 인자 뒤에 쉼표를 유지해야 해요. 연결된 이름 인자와 위치 인자 목록 사이의 공백은 선택이에요.
sub p($x, $y, :$translate, :$rotate) {};
p(1, 1, :translate, :rotate); # normal way
p 1, 1, :translate, :rotate; # also normal way
p(1, 1) :translate :rotate; # parentheses + chained named arguments
p(1, 1) :translate:rotate;
p(1, 1):translate:rotate;
p 1, 1, :translate :rotate; # dangling comma + chained named arguments
p 1, 1, :translate:rotate;
p 1, 1,:translate:rotate;
반환값 (Return values)
어떤 Block이나 Routine도 마지막 표현식의 값을 호출자에게 반환값으로 제공해요. return이나 return-rw를 호출하면 그 파라미터(있다면)가 반환값이 돼요. 기본 반환값은 Nil이에요.
sub a { 42 };
sub b { say a };
sub c { };
b; # OUTPUT: «42»
say c; # OUTPUT: «Nil»
여러 반환값은 목록으로 또는 Capture를 만들어 반환돼요. 여러 반환값을 풀려면 구조 분해(destructuring)를 쓰면 돼요.
sub a { 42, 'answer' };
put a.raku;
# OUTPUT: «(42, "answer")»
my ($n, $s) = a;
put [$s, $n];
# OUTPUT: «answer 42»
sub b { <a b c>.Capture };
put b.raku;
# OUTPUT: «\("a", "b", "c")»
반환 타입 제약 (Return type constraints)
Raku는 함수의 반환 타입을 지정하는 여러 방법이 있어요.
sub foo(--> Int) {}; say &foo.returns; # OUTPUT: «(Int)»
sub foo() returns Int {}; say &foo.returns; # OUTPUT: «(Int)»
sub foo() of Int {}; say &foo.returns; # OUTPUT: «(Int)»
my Int sub foo() {}; say &foo.returns; # OUTPUT: «(Int)»
다른 타입의 값을 반환하려 하면 컴파일 오류가 나요.
sub foo() returns Int { "a"; }; foo; # Type check fails
returns와 of는 동등하고, 둘 다 Callable의 트레잇을 선언하는 것이므로 타입만 받아요. 마지막 선언은 사실 타입 선언이라 당연히 타입만 받을 수 있어요. 반면 -->는 정의되지 않은 값과 정의된 값을 모두 받을 수 있어요.
Nil과 Failure는 반환 타입 제약에서 예외이며, 어떤 루틴에서든 제약과 무관하게 반환할 수 있다는 점을 주의하세요.
sub foo() returns Int { fail }; foo; # Failure returned
sub bar() returns Int { return }; bar; # Nil returned
다중 디스패치 (Multi-dispatch)
Raku는 같은 이름이지만 다른 시그니처를 가진 여러 루틴을 작성할 수 있어요. 루틴이 이름으로 호출되면 런타임 환경이 적절한 *후보(candidate)*를 결정해서 호출해요.
각 후보는 multi 키워드로 선언돼요. 디스패치는 파라미터의 arity(개수), 타입, 이름에 따라 일어나며 어떤 상황에서는 multi 선언 순서에 따라 달라져요. 다음 예제를 보세요.
# version 1
multi happy-birthday( $name ) {
say "Happy Birthday $name !";
}
# version 2
multi happy-birthday( $name, $age ) {
say "Happy {$age}th Birthday $name !";
}
# version 3
multi happy-birthday( :$name, :$age, :$title = 'Mr' ) {
say "Happy {$age}th Birthday $title $name !";
}
# calls version 1 (arity)
happy-birthday 'Larry'; # OUTPUT: «Happy Birthday Larry !»
# calls version 2 (arity)
happy-birthday 'Luca', 40; # OUTPUT: «Happy 40th Birthday Luca !»
# calls version 3
# (named arguments win against arity)
happy-birthday( age => '50', name => 'John' ); # OUTPUT: «Happy 50th Birthday Mr John !»
# calls version 2 (arity)
happy-birthday( 'Jack', 25 ); # OUTPUT: «Happy 25th Birthday Jack !»
happy-birthday sub의 처음 두 버전은 arity(인자 개수)만 다르고, 세 번째 버전은 이름 인자를 사용해요. 이름 인자가 쓰일 때만 선택되는데, arity가 다른 multi 후보와 같더라도 그렇죠.
두 sub의 arity가 같으면 인자 타입이 디스패치를 결정하고, 이름 인자가 있으면 그 타입이 다른 후보와 같더라도 이름 인자가 디스패치를 결정해요.
multi happy-birthday( Str $name, Int $age ) {
say "Happy {$age}th Birthday $name !";
}
multi happy-birthday( Str $name, Str $title ) {
say "Happy Birthday $title $name !";
}
multi happy-birthday( Str :$name, Int :$age ) {
say "Happy Birthday $name, you turned $age !";
}
happy-birthday 'Luca', 40; # OUTPUT: «Happy 40th Birthday Luca !»
happy-birthday 'Luca', 'Mr'; # OUTPUT: «Happy Birthday Mr Luca !»
happy-birthday age => 40, name => 'Luca'; # OUTPUT: «Happy Birthday Luca, you turned 40 !»
이름 파라미터는 호출에서 제공되지 않더라도 디스패치에 참여해요. 그래서 이름 파라미터를 가진 multi 후보가 우선하게 돼요.
타입 제약에 대한 더 많은 정보는 별도로 제공됩니다.
multi as-json(Bool $d) { $d ?? 'true' !! 'false'; }
multi as-json(Real $d) { ~$d }
multi as-json(@d) { sprintf '[%s]', @d.map(&as-json).join(', ') }
say as-json( True ); # OUTPUT: «true»
say as-json( 10.3 ); # OUTPUT: «10.3»
say as-json( [ True, 10.3, False, 24 ] ); # OUTPUT: «[true, 10.3, false, 24]»
일부 시그니처 차이(특히 where 절이나 subset을 쓸 때)에서는 multi 메서드/sub의 정의 순서가 사용되어 각 가능성을 차례로 평가해요. 예시는 아래 '정의 순서에 따른 multi 해석'을 보세요.
루틴 타입 지정 없이 multi를 쓰면 기본적으로 sub가 되지만, 메서드에도 쓸 수 있어요. 후보들은 전부 객체의 multi 메서드예요.
class Congrats {
multi method congratulate($reason, $name) {
say "Hooray for your $reason, $name";
}
}
role BirthdayCongrats {
multi method congratulate('birthday', $name) {
say "Happy birthday, $name";
}
multi method congratulate('birthday', $name, $age) {
say "Happy {$age}th birthday, $name";
}
}
my $congrats = Congrats.new does BirthdayCongrats;
$congrats.congratulate('promotion','Cindy'); # OUTPUT: «Hooray for your promotion, Cindy»
$congrats.congratulate('birthday','Bob'); # OUTPUT: «Happy birthday, Bob»
sub과 달리, multi 메서드에서 이름 파라미터를 쓰려면 기대한 대로 동작하게 하려면 필수 파라미터여야 해요.
non-multi sub이나 연산자는 부모 스코프나 자식 스코프에서 같은 이름의 multi 후보를 숨긴다는 점을 주의하세요. 임포트된 non-multi 후보도 마찬가지예요.
Multi-dispatch는 파라미터 트레잇에서도 동작하는데, is rw 파라미터가 있는 루틴이 그렇지 않은 루틴보다 우선해요.
proto þoo (|) {*}
multi þoo( $ðar is rw ) { $ðar = 42 }
multi þoo( $ðar ) { $ðar + 42 }
my $bar = 7;
say þoo($bar); # OUTPUT: «42»
proto
proto는 multi 후보들 사이의 공통점을 공식적으로 선언하는 방법이에요. 인자를 검증할 수 있지만 수정할 수는 없는 래퍼(wrapper) 역할을 해요. 기본 예제를 보죠.
proto congratulate(Str $reason, Str $name, |) {*}
multi congratulate($reason, $name) {
say "Hooray for your $reason, $name";
}
multi congratulate($reason, $name, Int $rank) {
say "Hooray for your $reason, $name -- got rank $rank!";
}
congratulate('being a cool number', 'Fred'); # OK
congratulate('being a cool number', 'Fred', 42); # OK
congratulate('being a cool number', 42); # Proto match error
proto는 모든 multi congratulate sub이 두 문자열, 선택적으로 그 뒤에 추가 파라미터라는 기본 시그니처를 지키도록 강제해요. |는 이름 없는 Capture 파라미터로, multi가 추가 인자를 받을 수 있게 해줘요. 처음 두 호출은 성공하지만 세 번째는 42가 Str과 매치되지 않아 (컴파일 시간에) 실패해요.
say &congratulate.signature # OUTPUT: «(Str $reason, Str $name, | is raw)»
proto에 함수 본문을 주고, 디스패치가 일어나길 원하는 곳에 {*}(중괄호 안에는 공백이 없음)를 놓을 수 있어요. 이는 루틴에 '구멍'이 있어서 주어진 인자에 따라 다른 동작을 할 때 유용해요.
# attempts to notify someone -- False if unsuccessful
proto notify(Str $user, Str $msg) {
my \hour = DateTime.now.hour;
if 8 < hour < 22 {
return {*};
} else {
# we can't notify someone when they might be sleeping
return False;
}
}
proto는 multi 후보의 래퍼이므로, 루틴의 multi 후보 시그니처가 proto의 것과 정확히 일치할 필요는 없어요. multi 후보의 인자는 proto의 것의 하위 타입일 수 있고, multi 후보의 반환 타입은 proto의 것과 완전히 달라도 돼요. 이렇게 다른 타입을 쓰는 건 특히 proto에 함수 본문을 줄 때 유용해요.
enum DebugType <LOG WARNING ERROR>;
#|[ Prints a message to stderr with a color-coded key. ]
proto debug(DebugType:D $type, Str:D $message --> Bool:_) {
note sprintf qb/\e[1;%dm[%s]\e[0m %s/, {*}, $type.key, $message
}
multi debug(LOG;; Str:D --> 32) { }
multi debug(WARNING;; Str:D --> 33) { }
multi debug(ERROR;; Str:D --> 31) { }
{*}는 항상 그 호출에 주어진 파라미터로 후보들에게 디스패치해요. 파라미터 기본값과 타입 강제(coercion)는 동작하지만 전달되지는 않아요.
proto mistake-proto(Str() $str, Int $number = 42) {*}
multi mistake-proto($str, $number) { say $str.^name }
mistake-proto(7, 42); # OUTPUT: «Int» -- not passed on
mistake-proto('test'); # fails -- not passed on
메서드에 proto를 쓰는 더 긴 예제는 공통 기능을 proto 메서드로 추출하는 법을 보여줘요.
class NewClass {
has $.debug is rw = False;
has $.value is rw = 'Initial value';
proto method handle( | ) {
note "before value is 「$.value」" if $.debug;
{*}
note "after value is 「$.value」" if $.debug;
}
multi method handle(Str $s) {
$.value = $s;
say 'in string'
}
multi method handle(Positional $s) {
$.value = $s[0];
say 'in positional'
}
multi method handle( $a, $b ) {
$.value = "$a is looking askance at $b";
say 'with more than one value'
}
}
my NewClass $x .= new;
$x.handle('hello world');
$x.handle(<hello world>);
$x.debug = True;
$x.handle('hello world');
$x.handle(<hello world>);
$x.handle('Claire', 'John');
# OUTPUT:
# in string
# in positional
# before value is 「hello」
# in string
# after value is 「hello world」
# before value is 「hello world」
# in positional
# after value is 「hello」
# before value is 「hello」
# with more than one value
# after value is 「Claire is looking askance at John」
only
sub이나 method 앞에 오는 only 키워드는 이 함수가 주어진 네임스페이스에 그 이름을 가진 유일한 함수가 될 것임을 나타내요.
only sub you () {"Can make all the world seem right"};
이렇게 하면 같은 네임스페이스의 다른 선언, 예를 들어
sub you ( $can ) { "Make the darkness bright" }
이런 것은 X::Redeclaration 타입의 예외로 실패해요. only는 모든 sub의 기본값이에요. 위 경우에 첫 번째 서브루틴을 only로 선언하지 않아도 정확히 같은 오류가 나요. 하지만 미래의 개발자가 proto를 선언하고 이름에 multi를 붙이는 것을 막을 수는 없어요. 루틴 앞에 only를 쓰는 것은 같은 네임스페이스에 같은 이름의 루틴을 선언하지 않겠다는 의도를 밝히는 방어적 프로그래밍 기능이에요.
(exit code 1)
===SORRY!=== Error while compiling /tmp/only-redeclaration.raku
Redeclaration of routine 'you' (did you mean to declare a multi-sub?)
at /tmp/only-redeclaration.raku:3
------> <BOL>⏏<EOL>
익명 sub는 only로 선언할 수 없어요. only sub {}는 놀랍게도 X::Anon::Multi 타입의 오류를 던져요.
정의 순서에 따른 multi 해석
파라미터 타입에 의한 분류가 명확한 매치를 찾기에 충분하지 않을 때, 메서드나 sub의 선언 순서대로 평가되는 몇 가지 다른 타이브레이커가 있어요. 여기에는 where 절과 subset, 이름 파라미터, 시그니처 언팩(unpack)이 포함돼요.
이 코드 예제에서 두 multi sub은 where 절로만 구분돼요. 둘 중 하나를 통과시킬 수 있는 모호한 경우인 값 4가 있죠. 이 경우 먼저 정의된 multi sub이 이겨요.
{
multi check_range ( Int $n where {$_ > 3} ) {
return "over 3";
};
multi check_range ( Int $n where {$_ < 5} ) {
return "under 5";
};
say check_range(4); # OUTPUT: over 3
}
{
multi check_range ( Int $n where {$_ < 5} ) {
return "under 5";
};
multi check_range ( Int $n where {$_ > 3} ) {
return "over 3";
};
say check_range(4); # OUTPUT: under 5
}
다음 예제에서는 세 개의 subset이 문자열을 특정 허용값으로 제한하는 데 쓰여요. 세 개 모두 서로 겹치는 부분이 있어요.
subset Monster of Str where { $_ eq any( <godzilla gammera ghidora> ) };
subset Hero of Str where { $_ eq any( <godzilla ultraman inframan> ) };
subset Knockoff of Str where { $_ eq any( <gammera inframan thorndike> ) };
{
multi speak (Monster $name) {
say "The monster, $name roars!";
}
multi speak (Hero $name) {
say "The hero, $name shouts!";
}
multi speak (Knockoff $name) {
say "The knockoff, $name, slinks away...";
}
speak('godzilla'); # OUTPUT: The monster, godzilla roars!
speak('gammera'); # OUTPUT: The monster, gammera roars!
speak('inframan'); # OUTPUT: The hero, inframan shouts!
}
여기서 'godzilla'는 Hero가 아니라 Monster로 취급돼요. Monster multi가 먼저 오기 때문이에요. 'gammera'와 'inframan' 둘 다 Knockoff로 취급되지 않는데, 그 multi가 마지막에 오기 때문이에요.
정의 순서는 Raku가 그들을 보는 순서라는 점을 명심하세요. 예를 들어 multi sub을 다른 모듈에서 임포트한 경우엔 순서를 알아보기 어려울 수 있어요. 코드베이스 조직이 복잡해질수록 이 예제처럼 subset을 타입으로 쓰는 것보다 객체 클래스가 더 잘 확장될 수 있어요.
관례와 관용구 (Conventions and idioms)
위에서 설명한 디스패치 시스템은 많은 유연성을 제공하지만, 대부분의 내장 함수와 많은 모듈이 따르는 몇 가지 관례가 있어요.
슬러피 관례 (Slurpy conventions)
이 관례 중 아마 가장 중요한 건 슬러피(slurpy) 목록 인자를 처리하는 방식이에요. 대부분의 경우 함수는 슬러피 목록을 자동으로 펼치지(flatten) 않아요. 드문 예외는 목록의 목록에 합리적인 동작이 없는 함수(예: chrs)거나, 확립된 관용구와 충돌하는 경우(예: push의 역인 pop)예요.
이러한 느낌을 따라가고 싶다면, 어떤 Iterable 인자든 **@ 슬러피를 사용해 요소별로 분해해야 해요. 두 가지 세부 사항이 있어요.
Scalar컨테이너 안의Iterable은 카운트되지 않아요.- 최상위에서
,로 만들어진List는Iterable하나로만 카운트돼요.
이는 **@ 대신 +나 +@를 가진 슬러피를 사용해서 달성할 수 있어요.
sub grab(+@a) { "grab $_".say for @a }
이것은 다음과 매우 가까운 것의 축약형이에요.
multi grab(**@a) { "grab $_".say for @a }
multi grab(\a) {
a ~~ Iterable and a.VAR !~~ Scalar ?? nextwith(|a) !! nextwith(a,)
}
이 결과는 "단일 인자 규칙" 으로 알려진 동작을 만들어요. 슬러피 함수를 호출할 때 이해하는 게 중요해요.
grab(1, 2); # OUTPUT: «grab 1grab 2»
grab((1, 2)); # OUTPUT: «grab 1grab 2»
grab($(1, 2)); # OUTPUT: «grab 1 2»
grab((1, 2), 3); # OUTPUT: «grab 1 2grab 3»
이것은 또한 사용자가 요청한 펼침이 서브리스트가 하나일 때든 여러 개일 때든 일관되게 느껴지게 해요.
grab(flat (1, 2), (3, 4)); # OUTPUT: «grab 1grab 2grab 3grab 4»
grab(flat $(1, 2), $(3, 4)); # OUTPUT: «grab 1 2grab 3 4»
grab(flat (1, 2)); # OUTPUT: «grab 1grab 2»
grab(flat $(1, 2)); # OUTPUT: «grab 1grab 2»
이런 경우에 바인딩 변수와 sigil 없는 변수를 섞는 건 약간의 요령이 필요해요. 바인딩 중에는 Scalar 중개자가 없기 때문이에요.
my $a = (1, 2); # Normal assignment, equivalent to $(1, 2)
grab($a); # OUTPUT: «grab 1 2»
my $b := (1, 2); # Binding, $b links directly to a bare (1, 2)
grab($b); # OUTPUT: «grab 1grab 2»
my \c = (1, 2); # Sigilless variables always bind, even with '='
grab(c); # OUTPUT: «grab 1grab 2»
단일 인자 규칙을 따르는 루틴 사용의 더 많은 예시는 list 서브루틴 문서를 참고하세요.
함수는 일급 객체 (Functions are first-class objects)
함수와 다른 코드 객체는 다른 객체처럼 값으로서 전달될 수 있어요.
코드 객체를 얻는 방법은 여러 가지가 있어요. 선언 지점에서 변수에 할당할 수 있고,
my $square = sub (Numeric $x) { $x * $x }
# and then use it:
say $square(6); # OUTPUT: «36»
기존의 이름 있는 함수를 &-sigil을 앞에 붙여 참조할 수도 있어요.
sub square($x) { $x * $x };
# get hold of a reference to the function:
my $func = &square
이것은 고차 함수(higher order functions), 즉 다른 함수를 입력으로 받는 함수에 매우 유용해요. 간단한 예로 각 입력 요소에 함수를 적용하는 map이 있어요.
sub square($x) { $x * $x };
my @squared = map &square, 1..5;
say join ', ', @squared; # OUTPUT: «1, 4, 9, 16, 25»
연산자에도 같은 것을 쓸 수 있어요. 다만 그 경우에는 infix:로 선언된 이름을 사용해야 해요.
my $exp := &infix:<**>; say $exp(7,3); # OUTPUT: «343»
연산자가 자동 생성된 경우에도 이렇게 할 수 있어요. 예를 들어 XX는 X 연산자에 X 메타연산자를 적용한 경우예요.
my $XX := &infix:<XX>; say $XX( [1,(2,3)] , [(4,5),6] );
# OUTPUT: «(((1 (4 5))) ((1 6)) (((2 3) (4 5))) (((2 3) 6)))»
결론적으로 연산자에서는 실제 정의 방식을 걱정할 필요 없이 &infix< >로 포인터를 잡으면 돼요.
중위 형태 (Infix form)
2개 인자를 가진 서브루틴을 중위 연산자처럼 호출하려면 [와 ]로 감싼 서브루틴 참조를 쓰면 돼요.
sub plus { $^a + $^b };
say 21 [&plus] 21;
# OUTPUT: «42»
클로저 (Closures)
Raku의 모든 코드 객체는 클로저예요. 즉 바깥 스코프의 어휘 변수를 참조할 수 있어요.
sub generate-sub($x) {
my $y = 2 * $x;
return sub { say $y };
# ^^^^^^^^^^^^^^ inner sub, uses $y
}
my $generated = generate-sub(21);
$generated(); # OUTPUT: «42»
여기서 $y는 generate-sub 안의 어휘 변수이고, 반환되는 내부 서브루틴이 그것을 사용해요. 내부 sub가 호출될 때쯤 generate-sub는 이미 종료했어요. 그런데도 내부 sub는 $y를 계속 사용할 수 있는데, 그 변수를 클로저로 감쌌기 때문이에요.
클로저의 또 다른 예는 map을 사용해 숫자 목록을 곱하는 건이에요.
my $multiply-by = 5;
say join ', ', map { $_ * $multiply-by }, 1..5; # OUTPUT: «5, 10, 15, 20, 25»
여기서 map에 전달된 블록은 바깥 스코프의 $multiply-by 변수를 참조하므로 그 블록은 클로저가 돼요.
클로저가 없는 언어는 map만큼 쉽고 강력한 고차 함수를 제공하기 어려워요.
루틴 (Routines)
루틴은 Routine 타입에 부합하는 코드 객체로, 가장 대표적으로 Sub, Method, Regex, Submethod가 있어요.
루틴은 Block이 제공하는 것에 더해 추가 기능을 가져요. multi로 올 수 있고, 감쌀(wrap) 수 있고, return으로 일찍 나갈 수 있어요.
my $keywords = set <if for unless while>;
sub has-keyword(*@words) {
for @words -> $word {
return True if $word (elem) $keywords;
}
False;
}
say has-keyword 'not', 'one', 'here'; # OUTPUT: «False»
say has-keyword 'but', 'here', 'for'; # OUTPUT: «True»
여기서 return은 호출된 블록만 나가는 게 아니라 루틴 전체를 나가요. 일반적으로 블록은 return에 대해 투명하며, 가장 바깥쪽 루틴에 붙어요.
루틴은 인라인될 수 있고 그래서 감싸기에 장애물이 될 수 있어요. use soft; 프래그마를 쓰면 인라인을 막아 런타임에 감싸기를 허용할 수 있어요.
sub testee(Int $i, Str $s){
rand.Rat * $i ~ $s;
}
sub wrap-to-debug(&c){
say "wrapping {&c.name} with arguments {&c.signature.raku}";
&c.wrap: sub (|args){
note "calling {&c.name} with {args.gist}";
my \ret-val := callwith(|args);
note "returned from {&c.name} with return value {ret-val.raku}";
ret-val
}
}
my $testee-handler = wrap-to-debug(&testee);
# OUTPUT: «wrapping testee with arguments :(Int $i, Str $s)»
say testee(10, "ten");
# OUTPUT: «calling testee with \(10, "ten")returned from testee with return value "6.151190ten"6.151190ten»
&testee.unwrap($testee-handler);
say testee(10, "ten");
# OUTPUT: «6.151190ten»
연산자 정의하기 (Defining operators)
연산자는 그저 재미있는 이름을 가진 서브루틴이에요. 그 재미있는 이름은 카테고리 이름(infix, prefix, postfix, circumfix, postcircumfix) 뒤에 콜론, 그리고 연산자 이름(하나 이상, circumfix와 postcircumfix의 경우 두 구성 요소)의 목록으로 구성돼요. 이 모든 연산자와 그 의미에 대한 자세한 설명은 이 표에 포함돼 있어요.
이것은 기존 연산자에 multi 후보를 추가하는 경우와 새 연산자를 정의하는 경우 모두에서 동작해요. 후자의 경우 새 서브루틴의 정의가 자동으로 새 연산자를 문법(grammar)에 설치해요. 다만 현재 어휘 스코프에서만요. use나 import로 연산자를 임포트하는 것도 사용 가능하게 만들어요.
# adding a multi candidate to an existing operator:
multi infix:<+>(Int $x, "same") { 2 * $x };
say 21 + "same"; # OUTPUT: «42»
# defining a new operator
sub postfix:<!>(Int $x where { $x >= 0 }) { [*] 1..$x };
say 6!; # OUTPUT: «720»
연산자 선언은 가능한 한 빨리 사용 가능해지므로, 방금 정의한 연산자로 재귀할 수 있어요.
sub postfix:<!>(Int $x where { $x >= 0 }) {
$x == 0 ?? 1 !! $x * ($x - 1)!
}
say 6!; # OUTPUT: «720»
Circumfix와 postcircumfix 연산자는 열림·닫힘 두 개의 구분자로 만들어져요.
sub circumfix:<START END>(*@elems) {
"start", @elems, "end"
}
say START 'a', 'b', 'c' END; # OUTPUT: «(start [a b c] end)»
Postcircumfix는 자신이 해석된 뒤에 오는 항(term)도 인자로 받아요.
sub postcircumfix:<!! !!>($left, $inside) {
"$left -> ( $inside )"
}
say 42!! 1 !!; # OUTPUT: «42 -> ( 1 )»
블록은 연산자 이름에 직접 할당할 수 있어요. 변수 선언자를 쓰고 연산자 이름 앞에 &-sigil을 붙이면 돼요.
my &infix:<ieq> = -> |l { [eq] l>>.fc };
say "abc" ieq "Abc";
# OUTPUT: «True»
우선순위 (Precedence)
Raku의 연산자 우선순위는 기존 연산자에 상대적으로 지정돼요. is tighter, is equiv, is looser 트레잇을 연산자에 붙여 새 연산자의 우선순위가 다른 기존 연산자와 어떻게 관련되는지 나타낼 수 있어요. 여러 트레잇을 적용할 수도 있어요.
예를 들어 infix:<*>는 infix:<+>보다 더 tight한 우선순위를 가져요. 그 사이에 하나를 끼워 넣는 건 이렇게 해요.
sub infix:<!!>($a, $b) is tighter(&infix:<+>) {
2 * ($a + $b)
}
say 1 + 2 * 3 !! 4; # OUTPUT: «21»
여기서 1 + 2 * 3 !! 4는 1 + ((2 * 3) !! 4)로 해석돼요. 새 !! 연산자의 우선순위가 +와 * 사이에 있기 때문이에요.
같은 효과를 이렇게도 낼 수 있어요.
sub infix:<!!>($a, $b) is looser(&infix:<*>) { ... }
새 연산자를 기존 연산자와 같은 우선순위 레벨에 두려면 대신 is equiv(&other-operator)를 쓰면 돼요.
결합성 (Associativity)
같은 연산자가 연달아 여러 번 나타나면 여러 가능한 해석이 있어요. 예를 들어
1 + 2 + 3
이것은
(1 + 2) + 3 # left associative
로 해석될 수도 있고
1 + (2 + 3) # right associative
로 해석될 수도 있어요.
실수의 덧셈에서는 +가 수학적으로 결합적이므로 그 구분은 다소 무의미해요.
하지만 다른 연산자에서는 큰 차이가 있어요. 예를 들어 거듭제곱 연산자 infix:<**>는요.
say 2 ** (2 ** 3); # OUTPUT: «256»
say (2 ** 2) ** 3; # OUTPUT: «64»
Raku는 다음과 같은 가능한 결합성 구성을 가져요.
| 결합성 (Associativity) | $a ! $b ! $c 의 의미 |
|---|---|
| left | ($a ! $b) ! $c |
| right | $a ! ($b ! $c) |
| non | ILLEGAL |
| chain | ($a ! $b) and ($b ! $c) |
| list | infix:<!>($a; $b; $c) |
연산자 결합성에 대한 추가 내용은 Operator 문서에 있어요.
is assoc 트레잇으로 중위 연산자의 결합성을 지정할 수 있고, left가 기본 결합성이에요. 중위가 아닌 연산자의 결합성 지정은 계획됐지만 아직 구현되지 않았어요.
sub infix:<>(*@a) is assoc<list> {
'(' ~ @a.join('|') ~ ')';
}
say 1 2 3; # OUTPUT: «(1|2|3)»
트레잇 (Traits)
트레잇(trait) 은 컴파일 시간에 실행되어 타입, 변수, 루틴, 속성, 또는 다른 언어 객체의 동작을 수정하는 서브루틴이에요.
트레잇의 예는 다음과 같아요.
class ChildClass is ParentClass { ... }
# ^^ trait, with argument ParentClass
has $.attrib is rw;
# ^^^^^ trait with name 'rw'
class SomeClass does AnotherRole { ... }
# ^^^^ trait
has $!another-attribute handles <close>;
# ^^^^^^^ trait
... 그리고 앞 섹션의 is tighter, is looser, is equiv, is assoc도 있어요.
트레잇은 trait_mod<VERB> 형태로 선언된 sub인데, VERB는 is, does, handles 같은 이름을 나타내요. 수정할 것(thing)을 인자로 받고, 이름을 이름 인자로 받아요. 자세한 내용은 Sub를 참고하세요.
multi trait_mod:<is>(Routine $r, :$doubles!) {
$r.wrap({
2 * callsame;
});
}
sub square($x) is doubles {
$x * $x;
}
say square 3; # OUTPUT: «18»
내장 루틴 트레잇의 문서는 type Routine을 참고하세요.
재디스패치 (Re-dispatching)
루틴이 체인에서 다음 메서드를 호출하고 싶은 경우가 있어요. 그 체인은 클래스 계층 구조의 부모 클래스 목록일 수도, multi 디스패치의 덜 구체적인 multi 후보일 수도, wrap의 내부 루틴일 수도 있어요.
다행히도 재디스패치를 쉽게 해 주는 일련의 도구가 있어요.
Next 도구들
아래 표는 세 가지 속성과 관련된 next 도구들을 나타내요.
-
반환 (Returns): "Yes"는 반환하고 그 지점에서 코드가 계속된다는 뜻이에요. "No"는 이 함수로 절대 반환되지 않는다는 뜻이에요 — 호출에 내장된 "return"이 있는 것처럼요.
-
인자 (Arguments): 구체적으로, 현재 함수의 인자를 호출에서 재사용하는지, 아니면 몇몇 인자를 직접 지정해야 하는지를 나타내요.
nextcallee의 경우 인자가 필요 없어요. 대신 호출될 때 인자가 반환값에 전달되기 때문이에요. -
참조 대상 (Referee): 도구가 호출할 메서드예요 ("referrer"의 역). 다음 중 하나일 수 있어요.
- next: 체인의 다음 항목
- self: 재귀처럼 스스로를 다시 호출
- preset:
nextcallee가 저장한 항목을 호출
| Returns | Arguments | Referee* | |
|---|---|---|---|
| nextsame | No | Reuse | next |
| nextwith | No | Specify | next |
| callsame | Yes | Reuse | next |
| callwith | Yes | Specify | next |
| nextcallee | Yes | None | next |
| samewith | Yes | Specify | self |
| nextcallee's rv* | Yes | Specify | preset |
* nextcallee의 rv는 nextcallee를 호출해서 반환된 도구이며, 이것 자체를 호출할 수 있어요.
위로부터 배울 점은 다음과 같아요.
with는 항상 인자를 지정해야 한다는 뜻이에요 — 이건 일관돼요.- 단어 끝의
same은 현재 실행 중인 함수의 인자가 referee를 호출할 때 자동으로 재사용된다는 뜻이에요.samewith는 "same"을 포함하지만 인자가 재사용된다는 뜻은 아니라는 점을 주의하세요. 여기서 "same"이 referee를 가리키기 때문이에요 — 함수가 호출 스택의 다음이 아니라 자기 자신과 같은 메서드를 호출하는 거죠. next는 반환하지 않는다는 뜻이에요. 다만nextcallee는 반환해요.
sub callsame
callsame은 현재 후보에 사용된 것과 같은 인자로 다음 매치 후보를 호출하고 그 후보의 반환값을 반환해요.
proto a(|) {*}
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
my $res = callsame;
say "Back in Int with $res";
}
a 1; # OUTPUT: «Int 1Any 1Back in Int with 5»
sub callwith
callwith는 원래 시그니처와 매치하는 다음 후보, 즉 사용자가 제공한 인자로 사용될 수 있는 다음 함수를 호출하고 그 후보의 반환값을 반환해요.
proto a(|) {*}
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
my $res = callwith($x + 1);
say "Back in Int with $res";
}
a 1; # OUTPUT: «Int 1Any 2Back in Int with 5»
여기서 a 1은 가장 구체적인 Int 후보를 먼저 호출하고, callwith는 덜 구체적인 Any 후보로 재디스패치해요. $x + 1이 Int임에도 체인의 다음 후보를 호출한다는 점을 주의하세요.
이 경우에선 예를 들어 이렇게 돼요.
proto how-many(|) {*}
multi how-many( Associative $a ) {
say "Associative $a ";
my $calling = callwith( 1 => $a );
return $calling;
}
multi how-many( Pair $a ) {
say "Pair $a ";
return "There is $a "
}
multi how-many( Hash $a ) {
say "Hash $a";
return "Hashing $a";
}
my $little-piggy = little => 'piggy';
say $little-piggy.^name; # OUTPUT: «Pair»
say &how-many.cando( \( $little-piggy ));
# OUTPUT: «(sub how-many (Pair $a) { #`(Sub|68970512) ... } sub how-many (Associative $a) { #`(Sub|68970664) ... })»
say how-many( $little-piggy ); # OUTPUT: «Pair little piggyThere is little piggy»
사용자가 제공한 Pair 인자를 받는 후보는 처음 정의된 두 함수뿐이에요. Pair는 Hash로 쉽게 강제될 수 있지만, 시그니처가 이렇게 매치돼요.
say :( Pair ) ~~ :( Associative ); # OUTPUT: «True»
say :( Pair ) ~~ :( Hash ); # OUTPUT: «False»
우리가 제공한 인자는 Pair예요. 그것은 Hash와 매치되지 않으므로 대응 함수는 &how-many.cando( \( $little-piggy ));의 출력에서 보듯 후보 목록에 포함되지 않아요.
sub nextsame
nextsame은 현재 후보에 사용된 것과 같은 인자로 다음 매치 후보를 호출하고 절대 반환하지 않아요.
proto a(|) {*}
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
nextsame;
say "never executed because nextsame doesn't return";
}
a 1; # OUTPUT: «Int 1Any 1»
sub nextwith
nextwith는 사용자가 제공한 인자로 다음 매치 후보를 호출하고 절대 반환하지 않아요.
proto a(|) {*}
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
nextwith($x + 1);
say "never executed because nextwith doesn't return";
}
a 1; # OUTPUT: «Int 1Any 2»
sub samewith
samewith는 호출 지점에서 제공된 인자로 multi를 다시 호출하고 그 호출이 제공한 값을 반환해요. 자기 재귀에 쓸 수 있어요.
proto factorial(|) {*}
multi factorial(Int $x where * ≤ 1) { 1 }
multi factorial(Int $x) {
$x * samewith($x-1);
}
say (factorial 10); # OUTPUT: «36288000»
sub nextcallee
재디스패치가 현재 스코프가 아닌 블록을 호출해야 할 때가 있고, 이는 nextwith와 같은 친구들에게 잘못된 스코프를 가리키는 문제를 일으켜요. nextcallee로 올바른 후보를 포착하고 원하는 시점에 호출하면 돼요.
proto pick-winner(|) {*}
multi pick-winner (Int \s) {
my &nextone = nextcallee;
Promise.in(π²).then: { nextone s }
}
multi pick-winner { say "Woot! $^w won" }
with pick-winner ^5 .pick -> \result {
say "And the winner is...";
await result;
}
# OUTPUT:
# And the winner is...
# Woot! 3 won
Int 후보는 nextcallee를 잡은 다음, 일정 시간 후에 병렬로 실행될 Promise를 띄우고 반환해요. 여기서는 nextsame을 쓸 수 없어요. 원래 루틴 대신 Promise의 블록을 nextsame 하려는 셈이 되기 때문이에요.
이름과 달리 nextcallee 함수는 이렇게 동작한다는 점을 주의하세요.
- 파라미터를 받는다는 점에서
callwith/nextwith와 같아요. - 반환한다는 점에서
callwith/callsame과 같아요.nextcallee에 대한 호출은 참조를 반환하고, 그 참조에 대한 호출도 반환해요 (nextwith/nextsame은 반환하지 않아요).
감싸진 루틴 (Wrapped routines)
위에서 언급한 것들 외에도 재디스패치는 훨씬 더 많은 상황에서 유용해요. 예를 들어 감싸진 루틴으로 디스패치할 때요.
# enable wrapping:
use soft;
# function to be wrapped:
sub square-root($x) { $x.sqrt }
&square-root.wrap(sub ($num) {
nextsame if $num >= 0;
1i * callwith(abs($num));
});
say square-root(4); # OUTPUT: «2»
say square-root(-4); # OUTPUT: «0+2i»
부모 클래스의 루틴 (Routines of parent class)
또 다른 사용 사례는 부모 클래스의 메서드로 재디스패치하는 거예요.
say Version.new('1.0.2') # OUTPUT: v1.0.2
class LoggedVersion is Version {
method new(|c) {
note "New version object created with arguments " ~ c.raku;
nextsame;
}
}
say LoggedVersion.new('1.0.2');
# OUTPUT:
# New version object created with arguments \("1.0.2")
# v1.0.2
강제 타입 (Coercion types)
강제 타입(coercion type)은 루틴 인자에 특정 타입을 강제하면서도 루틴 자체는 더 넓은 입력을 받도록 허용해요. 호출되면 인자는 자동으로 더 엄격한 타입으로 좁혀져서, 루틴 안에서는 인자가 항상 원하는 타입을 가져요.
인자를 더 엄격한 타입으로 변환할 수 없는 경우 Type Check 오류가 던져져요.
sub double(Int(Cool) $x) {
2 * $x
}
say double '21';# OUTPUT: «42»
say double 21; # OUTPUT: «42»
say double Any; # Type check failed in binding $x; expected 'Cool' but got 'Any'
위 예제에서 Int는 인자 $x가 강제될 목표 타입이고, Cool은 루틴이 넓은 입력으로 받아들이는 타입이에요.
받아들이는 넓은 입력 타입이 Any라면 Int(Any) 강제를 Any 타입을 생략해 Int()로 줄여 쓸 수 있어요.
강제는 목표 타입과 같은 이름의 메서드를 찾는 방식으로 동작해요. 인자에 그런 메서드가 있으면 호출해서 후자를 기대하는 좁은 타입으로 변환해요. 위로부터 알 수 있듯, 필요한 메서드만 제공하면 사용자 타입 사이의 강제도 제공할 수 있어요.
class Bar {
has $.msg;
}
class Foo {
has $.msg = "I'm a foo!";
# allows coercion from Foo to Bar
method Bar {
Bar.new(:msg($.msg ~ ' But I am now Bar.'));
}
}
# wants a Bar, but accepts Any
sub print-bar(Bar() $bar) {
say $bar.^name; # OUTPUT: «Bar»
say $bar.msg; # OUTPUT: «I'm a foo! But I am now Bar.»
}
print-bar Foo.new;
위 코드에서 Foo 인스턴스가 print-bar에 인자로 전달되면 Foo.Bar 메서드가 호출되고 그 결과가 $bar에 들어가요.
강제 타입은 타입이 동작하는 어디서나 동작하도록 되어 있지만, Rakudo는 현재(2018.05) 시그니처에서만 파라미터와 반환 타입 둘 다에 구현해요.
강제는 반환 타입에서도 동작해요.
sub are-equal (Int $x, Int $y --> Bool(Int) ) { $x - $y };
for (2,4) X (1,2) -> ($a,$b) {
say "Are $a and $b equal? ", are-equal($a, $b);
} # OUTPUT: «Are 2 and 1 equal? TrueAre 2 and 2 equal? FalseAre 4 and 1 equal? TrueAre 4 and 2 equal? True»
이 경우 Int를 Bool로 강제하고, 그 결과를 함수를 호출하는 for 루프 안에서 출력해요(문자열 컨텍스트로 넣는 것이죠).
sub MAIN
sub MAIN을 선언하면 스크립트에 대한 명령줄 인터페이스를 쉽게 지정할 수 있어요.