타입 시스템(Type system)
타입 시스템(Type system) (typesystem)
Raku의 타입 시스템은 어떻게 움직이는지 이해하면, Raku 코드가 왜 그렇게 동작하는지 훨씬 잘 보여요. 이 문서에서는 타입 정의부터 클래스·역할·열거형·부분집합까지, 타입을 둘러싼 전체 그림을 하나씩 풀어 설명할게요.
Raku 타입의 정의
타입은 타입 객체(type object)를 만들어 새 객체를 정의해요. 그 타입 객체는 객체의 인스턴스를 만들거나 값이 타입에 맞는지 검사하는 인터페이스를 제공하죠. 어떤 타입 객체든 Any 또는 Mu의 하위 클래스예요. 인트로스펙션(introspection) 메서드는 그 기반 클래스들과 인트로스펙션 메타메서드(.^)를 통한 상속으로 제공돼요. 새 타입은 다음 타입 선언자 중 하나로 컴파일 타임에 현재 스코프에 도입되거나, 메타객체 프로토콜로 런타임에 도입돼요. 모든 타입 이름은 그 스코프에서 고유해야 해요.
기본 타입(Default types)
사용자가 타입을 제공하지 않으면 Raku는 타입이 Any라고 가정해요. 여기에는 컨테이너, 기반 클래스, 파라미터, 반환 타입이 포함돼요.
my $a = 1;
$a = Nil;
say $a.^name;
# OUTPUT: «Any»
class C {};
say C.^parents(:all);
# OUTPUT: «((Any) (Mu))»
컨테이너의 기본 타입은 Any지만 기본 타입 제약은 Mu예요. 바인딩은 값만이 아니라 컨테이너를 교체한다는 점을 주의하세요. 이 경우 타입 제약이 바뀔 수 있어요.
타입 객체(Type objects)
객체가 타입 객체인지 검사하려면 type smiley로 제약된 타입에 대해 스마트매치하거나 .DEFINITE 메서드를 쓰세요.
my $a = Int;
say $a ~~ Mu:U;
# OUTPUT: «True»
say not $a.DEFINITE;
# OUTPUT: «True»
.DEFINITE는 invocant가 인스턴스면 True를 돌려줘요. False를 돌려주면 invocant는 타입 객체인 거예요.
정의되지 않음(Undefinedness)
정의되지 않은 객체는 Raku에서 타입 정보를 유지해요. 타입 객체는 정의되지 않음과 정의되지 않은 값의 타입을 모두 나타내는 데 쓰여요. 일반적인 정의되지 않은 값을 제공하려면 Any를 쓰세요. 컨테이너와 인자의 기본 타입인 Any와의 구분이 필요하면 Mu를 쓰세요.
.CREATE로 만든 객체의 인스턴스는 관례상 정의되어 있어요. .defined 메서드는 정의됨을 나타내는 Bool::True를 돌려줘요. 그 규칙의 예외는 Nil과 Failure이에요. 어떤 객체든 .defined를 오버로드할 수 있고 그렇게 하면 추가 정보를 담을 수 있다는 점을 주의하세요. 또한 Raku는 정의됨(defineness)과 참(trueness)을 뚜렷하게 구분해요. 많은 값이 거짓이나 빈 의미를 담고 있음에도 정의되어 있어요. 그런 값으로 0, Bool::False, ()(빈 목록), NaN이 있어요.
값은 mixin을 통해 런타임에 정의되지 않은 상태가 될 수 있어요.
my Int $i = 1 but role :: { method defined { False } };
say $i // "undefined";
# OUTPUT: «undefined»
정의됨을 검사하려면 .defined를 호출하거나, //, with/without, 시그니처를 사용하세요.
강제 변환(Coercion)
한 타입을 다른 타입으로 바꾸는 것은 대상 타입과 같은 이름을 가진 강제 변환 메서드로 해요. 이 관례는 시그니처에 의해 의무화돼요. 소스 타입은 자신을 대상 타입으로 바꾸는 방법을 알아야 해요. 내장 타입이 자신을 사용자 정의 타입으로 바꾸게 하려면 augment이나 MOP을 사용하세요.
class C {
has $.int;
method this-is-c { put 'oi' x $!int ~ '‽' }
}
use MONKEY-TYPING;
augment class Int {
method C { C.new(:int(self))}
}
my $i = 10;
$i.=C;
$i.this-is-c();
# OUTPUT: «oioioioioioioioioioi‽»
Raku는 추가 연산을 적용하기 전에 대상 타입으로 변환하는 Cool에 정의된 메서드를 제공해요. 대부분의 내장 타입은 Cool에서 내려오므로, 원치 않을 수도 있는 암시적 강제 변환을 제공할 수 있어요. 그 메서드들을 함정 없이 사용하는 것은 사용자의 책임이에요.
my $whatever = "123.6";
say $whatever.round;
# OUTPUT: «124»
say <a b c d>.starts-with("ab");
# OUTPUT: «False»
타입 선언자(Type declarators)
타입 선언자는 주어진 스코프에 새 타입을 도입해요. 중첩된 스코프는 ::으로 구분할 수 있어요. 그런 스코프가 이미 없으면 새 패키지가 자동으로 만들어져요.
class Foo::Bar::C {};
put Foo::Bar::.keys;
# OUTPUT: «C»
전방 선언은 ...("stub" 연산자)만 담은 블록으로 제공할 수 있어요. 컴파일러는 현재 스코프의 끝에서 타입이 정의되었는지 검사해요.
class C {...}
# many lines later
class C { has $.attr }
class
class 선언자는 타입 객체로 컴파일되는 컴파일 타임 구성을 만들어요. 타입 객체는 단순한 Raku 객체로, 초기화자와 sub 메서드를 실행해 클래스와 어떤 부모 클래스에 선언된 모든 속성을 값으로 채워 인스턴스를 만드는 메서드를 제공해요. 초기화자는 속성 선언이나 생성자에서 제공할 수 있어요. 어떻게 실행할지 아는 것은 Metamodel::ClassHOW의 책임이에요. 이것이 Raku에서 객체를 만드는 유일한 마법 부분이에요. 기본 부모 타입은 Any이고, 그것은 다시 Mu에서 상속받아요. Mu가 관례상 그렇게 이름 붙은 기본 생성자 .new를 제공해요. 그 외에 .new는 특별한 의미를 담지 않고 특별히 취급되지도 않아요.
클래스 사용 방법에 대한 더 많은 정보는 Classes and objects 튜토리얼을 참고하세요.
믹스인(Mixins)
class로 도입된 타입은 런타임에 infix:<but>으로 확장할 수 있어요. 원본 타입은 수정되지 않고, 대신 새 타입 객체가 반환되어, 원본 타입이나 섞인 역할에 대해 타입 검사가 성공하는 컨테이너에 저장될 수 있어요.
class A {}
role R { method m { say 'oi‽' } }
my R $A = A but R;
my $a1 = $A.new;
$a1.m;
say [$A ~~ R, $a1 ~~ R];
# OUTPUT: «oi‽[True True]»
인트로스펙션
메타클래스
주어진 타입 객체가 클래스인지 검사하려면 메타객체 메서드 .HOW를 Metamodel::ClassHOW에 대해 검사하세요.
class C {};
say C.HOW ~~ Metamodel::ClassHOW;
# OUTPUT: «True»
비공개 속성(Private attributes)
비공개 Attribute는 트위길 $!, @!, %! 중 하나로 다뤄져요. 그것들은 공개 접근자 메서드가 자동으로 생성되지 않아요. 그래서 정의된 클래스 밖에서는 수정될 수 없어요.
class C {
has $!priv;
submethod BUILD { $!priv = 42 }
};
say (.name, .package, .has_accessor) for C.new.^attributes;
# OUTPUT: «($!priv (C) False)»
메서드(Methods)
method 선언자는 Method 타입의 객체를 정의하고 클래스의 스코프에서 제공된 이름에 바인딩해요. 클래스의 메서드는 기본적으로 has 스코프예요. our 스코프인 메서드는 기본적으로 메서드 캐시에 추가되지 않아서, 접근자 시길 $.으로 호출할 수 없어요. 완전 한정 이름과 invocant를 첫 인자로 두고 호출하세요.
상속과 multi
하위 클래스의 일반 메서드는 부모 클래스의 multi 메서드와 경쟁하지 않아요.
class Parent {
multi method m(Numeric $i){ say 'Numeric' }
multi method m(Int $i){ say 'Int' }
}
class Child is Parent {
method m(Numeric $i){ say 'Child::Numeric' }
}
my Int $i;
Child.new.m($i);
# OUTPUT: «Child::Numeric»
only 메서드
메서드가 multi 메서드가 아니라는 것을 명시적으로 표현하려면 only 메서드 선언자를 쓰세요.
class C {
only method m {};
multi method m {};
};
# OUTPUT: «X::Comp::AdHoc: Cannot have a multi candidate for 'm' when an only method is also in the package 'C'»
submethod BUILD
submethod BUILD는 .bless에 의해 (간접적으로) 호출돼요. 그것은 클래스의 비공개·공개 속성을 설정하기 위한 것이고, .bless에 전달된 모든 명명된 속성을 받아요. Mu에 정의된 기본 생성자 .new이 .bless를 호출하는 메서드예요. 공개 접근자 메서드는 BUILD에서 사용할 수 없으므로, 대신 비공개 속성 표기를 써야 해요.
class C {
has $.attr;
submethod BUILD (:$attr = 42) {
$!attr = $attr
};
multi method new($positional) {
self.bless(:attr($positional), |%_)
}
};
C.new.say; C.new('answer').say;
# OUTPUT: «C.new(attr => 42)
# C.new(attr => "answer")»
폴백 메서드(Fallback method)
특수 이름 FALLBACK을 가진 메서드는 이름을 해석하는 다른 수단이 결과를 내지 못할 때 호출돼요. 첫 인자는 이름을 담고, 뒤따르는 모든 인자는 원래 호출에서 전달돼요. multi 메서드와 sub-signatures가 지원돼요.
class Magic {
method FALLBACK ($name, |c(Int, Str)) {
put "$name called with parameters {c.raku}" }
};
Magic.new.simsalabim(42, "answer");
# OUTPUT: «simsalabim called with parameters ⌈\(42, "answer")⌋»
예약된 메서드 이름
일부 내장 인트로스펙션 메서드는 실제로 컴파일러가 제공하는 특수 문법이에요. 바로 WHAT, WHO, HOW, VAR, 그리고 (Rakudo v2026.01부터) WHERE, DEFINITE, REPR이죠. 그 이름으로 메서드를 선언하는 것은 조용히 실패해요. 동적 호출은 동작해서 외부 객체의 메서드를 호출할 수 있어요.
class A {
method WHAT { "ain't gonna happen" }
};
say A.new.WHAT; # OUTPUT: «(A)»
say A.new."WHAT"() # OUTPUT: «ain't gonna happen»
패키지 스코프의 메서드
our 스코프인 메서드는 클래스의 패키지 스코프에서 보여요.
class C {
our method packaged {};
method loose {}
};
say C::.keys
# OUTPUT: «(&packaged)»
이름이 같은 변수·메서드로 속성 설정
attr => $attr 또는 :attr($attr)라고 쓰는 대신, 속성을 설정하는 데 쓰는 변수(또는 메서드 호출)가 속성과 이름을 공유하면 타이핑을 아낄 수 있어요.
class A { has $.i = 42 };
class B {
has $.i = "answer";
method m() { A.new(:$.i) }
# ^^^^ Instead of i => $.i or :i($.i)
};
my $a = B.new.m;
say $a.i; # OUTPUT: «answer»
$.i 메서드 호출의 이름이 i이고 속성도 i로 불리니까, Raku가 단축을 허용해요. :$var, :$!private-attribute, :&attr-with-code-in-it 등에도 똑같이 적용돼요.
trait is nodal
List 메서드를 표시해 하이퍼연산자가 이 메서드를 호출하기 위해 내부 Iterable로 내려가지 않게 해요. 이 trait는 일반적으로 최종 사용자가 쓰는 것이 아니에요. 핵심 List 타입을 하위 클래스화하거나 augment하지 않는 한요. 차이를 보여주기 위해, 첫 예는 is nodal인 메서드(elems)를, 두 번째 예는 nodal이 아닌 메서드(Int)를 사용해요.
say ((1.0, "2", 3e0), [^4], '5')».elems; # OUTPUT: «(3, 4, 1)»
say ((1.0, "2", 3e0), [^4], '5')».Int # OUTPUT: «((1 2 3) [0 1 2 3] 5)»
handles
multi trait_mod:<handles>(Attribute:D $target, $thunk)
클래스의 속성에 적용된 trait handles는 제공된 메서드 이름에 대한 모든 호출을 속성의 같은 이름을 가진 메서드에 위임해요. 속성이 참조하는 객체는 초기화되어야 해요. 호출이 위임되는 객체에 대한 타입 제약을 제공할 수 있어요.
class A { method m(){ 'A::m has been called.' } }
class B is A { method m(){ 'B::m has been called.' } }
class C {
has A $.delegate handles 'm';
method new($delegate){ self.bless(delegate => $delegate) }
};
say C.new(B.new).m(); # OUTPUT: «B::m has been called.»
메서드 이름 대신 Pair(이름 바꾸기용), 이름 또는 Pair의 목록, Regex, 또는 Whatever를 제공할 수 있어요. 후자의 경우 클래스 자체와 상속 체인에 있는 기존 메서드가 우선해요. 로컬 FALLBACK까지 검색해야 한다면 HyperWhatever를 사용하세요.
class A {
method m1(){ 'A::m1 has been called.' }
method m2(){ 'A::m2 has been called.' }
}
class C {
has $.delegate handles <m1 m2> = A.new()
}
say C.new.m2; # OUTPUT: «A::m2 has been called.»
class D {
has $.delegate handles /m\d/ = A.new()
}
say D.new.m1; # OUTPUT: «A::m1 has been called.»
class E {
has $.delegate handles (em1 => 'm1') = A.new()
}
say E.new.em1; # OUTPUT: «A::m1 has been called.»
class F {
# Delegates all methods from A
has A $.delegate handles *;
}
say F.new.m1; # OUTPUT: «A::m1 has been called.»
say F.new.m2; # OUTPUT: «A::m2 has been called.»
trait is
multi trait_mod:<is>(Mu:U $child, Mu:U $parent)
trait is는 클래스의 정의에서 부모 클래스로 추가될 타입 객체를 받아들여요. 다중 상속을 허용하려면 trait을 두 번 이상 적용할 수 있어요. 클래스에 부모를 추가하면 그들의 메서드가 대상 클래스로 가져와져요. 같은 메서드 이름이 여러 부모에 있으면, 먼저 추가된 부모가 이겨요.
is trait이 제공되지 않으면 기본값 Any가 부모 클래스로 사용돼요. 이는 모든 Raku 객체가 인트로스펙션과 기본 타입으로의 강제 변환을 위한 인터페이스를 제공하는 같은 기본 메서드 집합을 갖도록 강제해요.
class A {
multi method from-a(){ 'A::from-a' }
}
say A.new.^parents(:all).raku;
# OUTPUT: «(Any, Mu)»
class B {
method from-b(){ 'B::from-b ' }
multi method from-a(){ 'B::from-A' }
}
class C is A is B {}
say C.new.from-a();
# OUTPUT: «A::from-a»
is rw
sub trait_mod:<is>(Mu:U $type, :$rw!)
클래스의 trait is rw은 그 클래스의 모든 공개 속성에 쓰기 가능한 접근자 메서드를 만들어요.
class C is rw {
has $.a;
};
my $c = C.new.a = 42;
say $c; # OUTPUT: «42»
trait is required
multi trait_mod:<is>(Attribute $attr, :$required!)
multi trait_mod:<is>(Parameter:D $param, :$required!)
클래스 또는 역할의 속성을 필수로 표시해요. 객체 생성 시점에 속성이 초기화되지 않으면 X::Attribute::Required를 던져요.
class Correct {
has $.attr is required;
}
say Correct.new(attr => 42);
# OUTPUT: «Correct.new(attr => 42)»
class C {
has $.attr is required;
}
C.new;
CATCH { default { say .^name => .Str } }
# OUTPUT: «X::Attribute::Required => The attribute '$!attr' is required, but you did not provide a value for it.»
비공개 속성을 가진 클래스도 같은 오류를 낸다는 점을 주의하세요.
class D {
has $!attr is required;
}
D.new;
CATCH { default { say .^name => .Str } }
# OUTPUT: «X::Attribute::Required => The attribute '$!attr' is required, but you did not provide a value for it.»
왜 필요한지에 대한 이유를 is required의 인자로 제공할 수 있어요.
class Correct {
has $.attr is required("it's so cool")
};
say Correct.new();
# OUTPUT: «The attribute '$!attr' is required because it's so cool,but you did not provide a value for it.»
trait hides
trait hides는 재디스패치의 대상이 되지 않는 상속을 제공해요.
class A {
method m { say 'i am hidden' }
}
class B hides A {
method m { nextsame }
method n { self.A::m }
};
B.new.m; # No output
B.new.n; # OUTPUT: «i am hidden»
trait is hidden은 클래스가 재디스패치에서 자신을 숨기게 해요.
class A is hidden {
method m { say 'i am hidden' }
}
class B is A {
method m { nextsame }
method n { self.A::m }
}
B.new.m; # No output
B.new.n; # OUTPUT: «i am hidden»
is hidden으로 선언된 클래스는 또한 약간 다른 메서드 시그니처를 생성해요. 재디스패치를 용이하게 하기 위해, 전형적인 메서드에는 추가 명명 인자를 캡처하는 추가 *%_ 파라미터가 자동으로 제공되요. is hidden으로 선언된 클래스는 재디스패치에 참여하지 않으므로, 그들의 메서드는 이 추가 파라미터를 받지 않아요.
trait trusts
한 클래스가 다른 클래스의 비공개 메서드에 접근하게 하려면 trait trusts를 쓰세요. 신뢰되는 클래스의 전방 선언이 필요할 수 있어요.
class B {...};
class A {
trusts B;
has $!foo;
method !foo { return-rw $!foo }
method raku { "A.new(foo => $!foo)" }
};
class B {
has A $.a .= new;
method change { $!a!A::foo = 42; self }
};
say B.new.change;
# OUTPUT: «B.new(a => A.new(foo => 42))»
클래스 augment하기
컴파일 타임에 클래스에 메서드와 속성을 추가하려면 클래스 정의 조각 앞에 augment를 쓰세요. 컴파일러는 같은 스코프의 앞부분에서 pragma use MONKEY-TYPING 또는 use MONKEY를 요구할 거예요. 성능 영향이 있을 수 있으므로 pragma가 붙는다는 점을 주의하세요.
use MONKEY; augment class Str {
method mark(Any :$set){
state $mark //= $set; $mark
}
};
my $s = "42";
$s.mark(set => "answer");
say $s.mark
# OUTPUT: «answer»
클래스 조각 안에서 할 수 있는 일에는 제한이 거의 없어요. 그중 하나는 메서드나 sub를 multi로 재선언하는 것이에요. 추가된 속성 사용은 구현되지 않아요. 명명된 파라미터에서만 다른 multi 후보를 추가하면, 그 후보가 이미 정의된 것 뒤에 추가되어 디스패처가 그것을 선택하지 않는다는 점을 주의하세요.
role
역할은 클래스가 공유하는 인터페이스의 정의를 허용하는 클래스 조각이에요. role 선언자도 타입 검사에 쓸 수 있는 타입 객체를 도입해요. 역할은 컴파일 타임과 런타임에 클래스와 객체에 섞일(믹스인) 수 있어요. role 선언자는 생성된 타입 객체를 반환해서 익명 역할과 제자리 믹스인의 정의를 허용해요.
role Serialize {
method to-string { self.Str }
method to-number { self.Num }
}
class A does Serialize {}
class B does Serialize {}
my Serialize @list;
@list.push: A.new;
@list.push: B.new;
say @list».to-string;
# OUTPUT: «[A<57192848> B<57192880>]»
메서드 본문의 유일한 요소로 ...을 쓰면 메서드를 추상으로 선언해요. 그런 메서드가 섞인 어떤 클래스든 그것을 오버로드해야 해요. 컴파일 단위의 끝 전에 메서드가 오버로드되지 않으면 X::Comp::AdHoc이 던져져요.
EVAL 'role R { method overload-this(){...} }; class A does R {}; ';
CATCH { default { say .^name, ' ', .Str } }
# OUTPUT: «X::Comp::AdHoc Method 'overload-this' must be implemented by A because it is required by roles: R.»
자동 펀(Auto-punning)
역할을 클래스 대신 객체를 만드는 데 쓸 수 있어요. 역할은 런타임에 존재할 수 없으므로, 같은 이름의 클래스가 생성되고 그것은 역할에 대해 타입 검사가 성공해요.
role R { method m { say 'oi‽' } };
R.new.^mro.say;
# OUTPUT: «((R) (Any) (Mu))»
say R.new.^mro[0].HOW.^name;
# OUTPUT: «Perl6::Metamodel::ClassHOW»
say R.new ~~ R;
# OUTPUT: «True»
trait does
trait does는 역할과 클래스에 적용되어 컴파일 타임 믹스인을 제공해요. 아직 정의되지 않은 역할을 가리키려면 전방 선언을 쓰세요. 역할이 섞인 클래스의 타입 이름은 믹스인을 반영하지 않아요. 타입 검사가 반영하죠. 둘 이상의 섞인 역할에서 메서드가 제공되면 먼저 정의된 메서드가 우선해요. 쉼표로 구분된 역할 목록을 제공할 수 있어요. 이 경우 충돌은 컴파일 타임에 보고돼요.
role R2 {...};
role R1 does R2 {};
role R2 {};
class C does R1 {};
say [C ~~ R1, C ~~ R2];
# OUTPUT: «[True True]»
파라미터화(Parameterized)
역할 이름 뒤의 [] 사이에 파라미터를 역할에 제공할 수 있어요.
role R[$d] { has $.a = $d };
class C does R["default"] { };
my $c = C.new;
say $c;
# OUTPUT: «C.new(a => "default")»
파라미터는 타입 제약을 가질 수 있어요. where 절은 타입에 지원되지 않지만 subset으로 구현할 수 있어요. 타입 캡처는 지원돼요.
class A {};
class B {};
subset A-or-B where * ~~ A|B;
role R[A-or-B ::T] {};
R[A.new].new;
기본 파라미터를 제공할 수 있어요.
role R[$p = fail("Please provide a parameter to role R")] {};
my $i = 1 does R;
CATCH { default { say .^name, ': ', .Str} }
# OUTPUT: «X::AdHoc: Could not instantiate role 'R':Please provide a parameter to role R»
타입 제약으로서
역할은 타입이 기대되는 곳이면 어디서든 타입 제약으로 쓰일 수 있어요. 역할이 does나 but으로 섞이면, 그 타입 객체가 해당 객체의 타입 객체 목록에 추가돼요. 역할이 클래스 대신 쓰이면(자동 펀 사용), 역할과 같은 이름을 가진 자동 생성 클래스의 타입 객체가 상속 체인에 추가돼요.
role Unitish[$unit = fail('Please provide a SI unit quantifier as a parameter to the role Unitish')] {
has $.SI-unit-symbol = $unit;
method gist {
given self {
# ...
when * < 1 { return self * 1000 ~ 'm' ~ $.SI-unit-symbol }
when * < 1000 { return self ~ $.SI-unit-symbol }
when * < 1_000_000 { return self / 1_000 ~ 'k' ~ $.SI-unit-symbol }
# ...
}
}
}
role SI-second does Unitish[<s>] {}
role SI-meter does Unitish[<m>] {}
role SI-kilogram does Unitish[<g>] {}
sub postfix:<s>(Numeric $num) { ($num) does SI-second }
sub postfix:<m>(Numeric $num) { ($num) does SI-meter }
sub postfix:<g>(Numeric $num) { ($num) does SI-kilogram }
sub postfix:<kg>(Numeric $num){ ($num * 1000) does SI-kilogram }
constant g = 9.806_65;
role SI-Newton does Unitish[<N>] {}
multi N(SI-kilogram $kg, SI-meter $m, SI-second $s --> SI-Newton ){ ($kg * ($m / $s²)) does SI-Newton }
multi N(SI-kilogram $kg --> SI-Newton) { ($kg * g) does SI-Newton }
say [75kg, N(75kg)];
# OUTPUT: «[75kg 735.49875kN]»
say [(75kg).^name, N(75kg).^name];
# OUTPUT: «[Int+{SI-kilogram} Rat+{SI-Newton}]»
enum
열거형은 연관된 타입을 가진 상수 키-값 쌍을 제공해요. 어떤 키든 그 타입이고 현재 스코프에 심볼로 주입돼요. 심볼이 사용되면 상수 표현식으로 취급되어, 심볼은 enum 쌍의 값으로 대체돼요. 어떤 열거형이든 역할 Enumeration에서 메서드를 상속받아요. 키-값 쌍을 생성하는 복잡한 표현식은 지원되지 않아요. 일반적으로 enum은 요소들이 Enumeration 역할이 섞인 Map인데, 이 역할은 각 요소에 대해 맵에 순서를 만드는 인덱스를 포함해요.
심볼의 문자열화는 문자열 컨텍스트에서 자동으로 이루어지고, 그것은 enum 쌍의 키이기도 한 그 이름과 정확히 같아요.
enum Names ( name1 => 1, name2 => 2 );
say name1, ' ', name2; # OUTPUT: «name1 name2»
say name1.value, ' ', name2.value; # OUTPUT: «1 2»
심볼 비교는 타입 정보와 enum 쌍의 값을 사용해요. 값 타입으로 Num과 Str이 지원돼요.
enum Names ( name1 => 1, name2 => 2 );
sub same(Names $a, Names $b){
$a eqv $b
}
say same(name1, name1); # OUTPUT: «True»
say same(name1, name2); # OUTPUT: «False»
my $a = name1;
say $a ~~ Names; # OUTPUT: «True»
say $a.^name; # OUTPUT: «Names»
모든 키는 같은 타입이어야 해요.
enum Mass ( mg => 1/1000, g => 1/1, kg => 1000/1 );
say Mass.enums;
# OUTPUT: «Map.new((g => 1, kg => 1000, mg => 0.001))»
그리고 어떤 종류의 심볼이든 쓸 수 있어요.
enum Suit <♣ ♦ ♥ ♠>;
전체 문법으로 그 심볼을 참조하기만 하면 돼요.
say Suit::<♣>; # OUTPUT: «♣»
my $heart = '♥';
say Suit::«$heart»; # OUTPUT: «♥»
그 문법 없이 unicode enum 키에 접근하려고 하면 오류가 나요.
say ♣ ; # OUTPUT: «(exit code 1) ===SORRY!===Argument to "say" seems to be malformed…
값이 주어지지 않으면 Int가 값 타입으로 가정되고, 0에서 시작해 키마다 1씩 증가해요. enum 키 타입으로 Int, Num, Rat, Str이 지원돼요.
enum Numbers <one two three four>;
say Numbers.enums;
# OUTPUT: «Map.new((four => 3, one => 0, three => 2, two => 1))»
다른 시작 값을 제공할 수 있어요.
enum Numbers «:one(1) two three four»;
say Numbers.enums;
# OUTPUT: «Map.new((four => 4, one => 1, three => 3, two => 2))»
이것을 () 형태의 초기화자로도 할 수 있지만, 값이 없는 키는 인용해야 해요.
enum Numbers (
one => 1,
'two',
'three',
'four'
);
Enum은 익명일 수도 있는데, 명명된 enum과의 유일한 차이는 그것을 Signature에서 쓰거나 변수를 선언하는 데 쓸 수 없다는 것이에요.
my $e = enum <one two three>;
say two; # OUTPUT: «two»
say one.^name; # OUTPUT: «»
say $e.^name; # OUTPUT: «Map»
정의된 심볼의 키와 값에 접근하는 다양한 메서드가 있어요. 그것들 모두 값을 Str로 바꾸는데, 바람직하지 않을 수 있어요. enum을 패키지로 취급하면 키의 타입 목록을 얻을 수 있어요.
enum E <one two>;
my @keys = E::.values;
say @keys.map: *.raku;
# OUTPUT: «(E::one E::two)» or «(E::two E::one)»
위 출력이 나타내듯 enum의 반복 순서는 보장되지 않아요. 그 이유는 enum을 "반복하는" 메서드들이 enum(결국 순회 가능하지 않음)을 직접 반복하지 않기 때문이에요. 대신 그 반복 메서드들은 enum과 같은 키와 값을 가진 Map을 만들어요. Map은 순서 없는 반복만 제공하므로, enum의 반복 메서드도 그렇게 해요. enum을 순서대로 반복해야 한다면 E.enums.sort(*.value)처럼 그 value로 정렬할 수 있어요.
() 괄호를 쓰면 enum을 임의의 동적으로 정의된 목록으로 정의할 수 있어요. 목록은 Pair 객체로 이루어져야 해요. 예를 들어 config 파일에 이렇게 있다면:
a 1
b 2
이 코드를 쓰는 enum을 만들 수 있어요.
enum ConfigValues ('config'.IO.lines.map({ my ($key, $value) = $_.words; $key => $value }));
say ConfigValues.enums; # OUTPUT: «Map.new((a => 1, b => 2))»
먼저 config 파일에서 줄을 읽고, words 메서드로 각 줄을 나누고, 각 줄에 대해 결과 페어를 돌려줘서 Pairs의 List를 만드는 거예요.
열거형 타입 지정(Typing Enums)
명시적 스코프로 enum을 선언할 때 타입을 제공할 수 있고, 그것은 enum 값의 타입 검사에 쓰여요.
my Str enum Foo (foo => 'foo');
모든 enum 쌍은 Enumeration으로 타입이 지정돼요. 게다가 enum 값이 Numeric, Stringy, 또는 이 둘의 조합으로 타입이 지정되면, enum 쌍은 각각 NumericEnumeration, StringyEnumeration, NumericStringyEnumeration 역할도 수행해요. 이것들은 단순히 Str 메서드로 enum 쌍이 어떻게 문자열화되는지 결정해요.
이 타입들이 역할이라는 점을 감안하면, 당연히 enum을 선언할 때 자신만의 역할을 제공해 커스텀 동작과 상태를 줄 수 있어요. 예를 들어 숫자가 비트마스크 enum의 플래그와 일치하는지 검사하는 것을 더 간단하게 하려면, 스마트매칭으로 이를 처리하는 ACCEPTS 메서드를 가진 BitmaskEnumeration 역할을 쓸 수 있어요.
role BitmaskEnumeration {
multi method ACCEPTS(::?CLASS:D: Int:D $value --> Bool:D) {
so $value +& self.value
}
}
enum Flags does BitmaskEnumeration (
FLAG_FOO => 0b001,
FLAG_BAR => 0b010,
FLAG_BAZ => 0b100,
);
say 0b111 ~~ FLAG_FOO & FLAG_BAR & FLAG_BAZ; # OUTPUT: «True»
메타클래스
주어진 타입 객체가 enum인지 검사하려면 메타객체 메서드 .HOW를 Metamodel::EnumHOW에 대해 검사하거나, 단순히 Enumeration 역할에 대해 검사하세요.
enum E(<a b c>);
say E.HOW ~~ Metamodel::EnumHOW; # OUTPUT: «True»
say E ~~ Enumeration; # OUTPUT: «True»
메서드
enum 타입과 enum 쌍에서 사용 가능한 메서드는 Enumeration 역할을 참고하세요.
강제 변환(Coercion)
enum 요소의 값을 그 적절한 enum 객체로 강제 변환하려면 enum 이름의 강제 변환자를 쓰세요.
my enum A (sun => 42, mon => 72);
A(72).pair.say; # OUTPUT: «mon => 72»
A(1000).say; # OUTPUT: «(A)»
마지막 예는 그 값을 포함하는 enum 쌍이 없을 때 무슨 일이 일어나는지 보여줘요.
module
모듈은 보통 클래스·역할·grammar·서브루틴·변수 같은 Raku 구성을 노출하는 하나 이상의 소스 파일이에요. 모듈은 보통 다른 Raku 프로그램에서 쓸 수 있는 라이브러리로서 Raku 코드를 배포하는 데 쓰여요.
전체 설명은 Modules 및 연결된 페이지를 참고하세요.
package
패키지는 명명된 프로그램 요소의 중첩된 네임스페이스예요. 모듈·클래스·grammar 모두 패키지의 한 종류예요. 전체 설명은 Packages를 참고하세요.
grammar
Grammar는 텍스트 파싱을 위한 특정 종류의 클래스예요. Grammar는 rule·token·regex로 구성되는데, grammar는 클래스이므로 그것들은 실제로 메서드예요. 전체 설명은 Grammars를 참고하세요.
subset
subset은 기반 타입으로 재디스패치할 새 타입을 선언해요. where 절이 제공되면 어떤 할당이든 주어진 코드 객체에 대해 검사돼요.
subset Positive of Int where * > -1;
my Positive $i = 1;
$i = -42;
CATCH { default { put .^name,': ', .Str } }
# OUTPUT: «X::TypeCheck::Assignment: Type check failed in assignment to $i; expected Positive but got Int (-42)»
Subset은 시그니처에서 쓸 수 있어요. 예를 들어 출력을 타입으로:
subset Foo of List where (Int,Str);
sub a($a, $b, --> Foo) { $a, $b }
# Only a List with the first element being an Int and the second a Str will pass the type check.
a(1, "foo"); # passes
a("foo", 1); # fails
기반 타입을 생략하면 기본값은 Any예요. 그래서 다음 두 개는 동등해요.
subset A-or-B where * ~~ A | B
subset A-or-B of Any where * ~~ A | B
Subset은 익명일 수 있어서, subset이 필요한데 이름이 필요하지 않거나 바람직하지 않은 곳에 인라인으로 배치할 수 있어요.
my enum E1 <A B>;
my enum E2 <C D>;
sub g(@a where { .all ~~ subset :: where E1|E2 } ) {
say @a
}
g([A, C]);
# OUTPUT: «[A C]»
Subset은 타입을 동적으로 검사하는 데도 쓸 수 있어요. require과 함께 쓰면 유용해요.
require ::('YourModule');
subset C where ::('YourModule::C');
버전, 저자, API 버전
타입을 선언할 때 버전, 저자, API 번호를 넘길 수 있고, 언제든 인트로스펙션할 수 있어요. 타입의 버전·저자·API 번호는 각각 부사 :ver<>, :auth<>, :api<>로 적용할 수 있어요. 그것들 모두 문자열을 인자로 받아요. :ver의 경우 문자열이 Version 객체로 변환되고, :api의 경우 문자열이 allomorph IntStr 객체로 변환돼요. :auth는 일반적으로 github:github-user나 gitlab:gitlab-user처럼 hosting:ID 형태를 취해요.
타입의 버전·저자·API 버전을 조회하려면 각각 .^ver, .^auth, .^api을 사용하세요. 아래는 class를 조회한 예시예요.
class C:ver<4.2.3>:auth<github:jane>:api<1> {}
say C.^ver; # OUTPUT: «v4.2.3»
say C.^ver.parts; # OUTPUT: «(4 2 3)»
say C.^auth; # OUTPUT: «github:jane»
say C.^api; # OUTPUT: «1»
비슷한 방식으로 role, grammar, module도 앞서 언급한 정보를 조회할 수 있어요.