Raku의 객체 지향 프로그래밍
Raku의 객체 지향 프로그래밍 (Objects)
Raku는 객체 지향 프로그래밍(OOP)을 강하게 지원해요. Raku는 여러 패러다임으로 프로그래밍할 수 있게 해 주지만, 그 중심에는 객체 지향 프로그래밍이 있어요.
Raku에는 미리 정의된 타입이 풍부한데, 보통 타입과 네이티브 타입 둘로 나눌 수 있어요. 변수에 저장할 수 있는 모든 것은 네이티브 값이거나 객체예요. 리터럴, 타입(타입 객체), 코드, 컨테이너까지 포함하죠.
네이티브 타입은 저수준 타입(예: uint64)에 쓰여요. 네이티브 타입이 객체와 같은 능력은 없지만, 메서드를 호출하면 자동으로 일반 객체로 *박싱(box)*돼요.
네이티브 값이 아닌 모든 것은 객체예요. 객체는 상속과 캡슐화를 모두 지원해요.
본문
객체 사용하기 (Using objects)
객체에 메서드를 호출하려면 점(dot) 다음에 메서드 이름을 붙이면 돼요:
say "abc".uc;
# OUTPUT: «ABC»
이것은 Str 타입의 객체인 "abc"에 uc 메서드를 호출해요. 메서드에 인자를 주려면 메서드 뒤 괄호 안에 인자를 넣으면 돼요.
my $formatted-text = "Fourscore and seven years ago...".indent(8);
say $formatted-text;
# OUTPUT: « Fourscore and seven years ago...»
$formatted-text에는 8칸 들여쓴 위 텍스트가 들어 있어요.
여러 인자는 쉼표로 구분해요:
my @words = "Abe", "Lincoln";
@words.push("said", $formatted-text.comb(/\w+/));
say @words;
# OUTPUT: «[Abe Lincoln said (Fourscore and seven years ago)]»
마찬가지로 여러 인자는 메서드 뒤에 콜론을 두고 인자 목록을 쉼표로 구분해서 지정할 수도 있어요:
say @words.join('--').subst: 'years', 'DAYS';
# OUTPUT: «Abe--Lincoln--said--Fourscore and seven DAYS ago»
괄호 없이 인자를 넘기려면 메서드 뒤에 :을 둬야 하므로, 콜론이나 괄호 없는 메서드 호출은 인자 목록 없는 메서드 호출로 모호함 없이 해석돼요:
say 4.log: ; # OUTPUT: «1.38629436111989» ( natural logarithm of 4 )
say 4.log: +2; # OUTPUT: «2» ( base-2 logarithm of 4 )
say 4.log +2; # OUTPUT: «3.38629436111989» ( natural logarithm of 4, plus 2 )
스마트매치나 객체를 문자열로 보간하는 것처럼 메서드 호출로 안 보이는 많은 연산이 내부적으로 메서드 호출일 수 있어요.
메서드는 변경 가능한 컨테이너를 반환할 수 있어서, 메서드 호출의 반환값에 할당할 수 있어요. 이것이 객체의 읽기·쓰기 속성에 쓰는 방식이에요:
$*IN.nl-in = "\r\n";
여기서 $*IN 객체에 nl-in 메서드를 인자 없이 호출하고, 반환된 컨테이너에 = (item assignment) 연산자로 할당해요.
모든 객체는 타입 계층의 루트인 Mu 클래스의 메서드를 지원해요. 모든 객체는 Mu에서 파생되죠.
객체 동등성(equality)에는 구조 비교 연산자인 특정 연산자 eqv가 필요해요:
class Foo {
has $.bar;
has $.baz
};
my $bar = "42";
my $baz = 24;
my $zipi = Foo.new( :$baz, :$bar);
my $zape = Foo.new( :$bar, :$baz);
say $zipi eqv $zape; # OUTPUT: «True»
반면 객체 정체성(identity)은 ===을 써요. 위에서 ===을 쓰면 예를 들어 False가 나와요.
타입 객체 (Type objects)
타입 자체도 객체이고, 타입 객체는 그 이름을 적어서 얻을 수 있어요:
my $int-type-obj = Int;
어떤 것이든 메서드 형태의 매크로인 WHAT 메서드를 호출해서 타입 객체를 요청할 수 있어요:
my $int-type-obj = 1.WHAT;
(Mu)가 아닌 타입 객체는 === 정체성 연산자로 동등성을 비교할 수 있어요:
sub f(Int $x) {
if $x.WHAT === Int {
say 'you passed an Int';
}
else {
say 'you passed a subtype of Int';
}
}
하지만 대부분의 경우 .isa 메서드면 충분해요:
sub f($x) {
if $x.isa(Int) {
...
}
...
}
부분타입(subtype) 검사는 스마트매치로 해요:
if $type ~~ Real {
say '$type contains Real or a subtype thereof';
}
클래스 (Classes)
클래스는 class 키워드로 선언하고, 보통 그 뒤에 이름이 와요.
class Journey { }
이 선언은 현재 패키지와 현재 어휘 스코프에 Journey라는 이름으로 타입 객체를 만들고 설치해요. 클래스를 어휘적으로 선언할 수도 있어요:
my class Journey { }
이러면 현재 어휘 스코프로 가시성이 제한돼요. 클래스가 모듈이나 다른 클래스 안에 중첩된 구현 세부 사항일 때 유용하죠.
속성 (Attributes)
속성(attribute)은 클래스의 인스턴스마다 존재하는 변수예요. 값으로 인스턴스화되면 변수와 값의 연관을 속성(property)이라고 불러요. 객체의 상태가 저장되는 곳이죠. Raku에서 모든 속성은 private이에요. 즉 클래스 인스턴스 자신만 직접 접근할 수 있어요. 보통 has 선언자와 ! twigil로 선언해요.
class Journey {
has $!origin;
has $!destination;
has @!travelers;
has $!notes;
}
또는 twigil을 생략할 수도 있어요. 그렇게 해도 private 속성(! twigil 포함)이 생기고, twigil 없는 이름(그 속성을 가리키는 별명)도 함께 만들어져요. 그래서 위 클래스를 이렇게 선언할 수도 있어요:
class Journey {
has $origin;
has $destination;
has @travelers;
has $notes;
}
이렇게 선언하면 나중에 twigil을 붙이든 안 붙이든 속성에 접근할 수 있어요. 예를 들어 $!origin과 $origin은 같은 속성을 가리켜요.
공개(public) 속성(protected조차도) 같은 건 없지만, 접근자 메서드를 자동 생성하는 방법은 있어요: ! twigil을 . twigil로 바꾸면 돼요(.는 메서드 호출을 떠올리게 하죠).
class Journey {
has $.origin;
has $.destination;
has @!travelers;
has $.notes;
}
이것은 기본적으로 읽기 전용 접근자를 제공해요. 속성 변경을 허용하려면 is rw 트레이트(trait)를 추가해요:
class Journey {
has $.origin;
has $.destination;
has @!travelers;
has $.notes is rw;
}
이제 Journey 객체를 만든 뒤 .origin, .destination, .notes는 모두 클래스 밖에서 접근 가능하지만, 수정할 수 있는 건 .notes뿐이에요.
origin이나 destination 같은 속성 없이 객체를 인스턴스화하면 원하는 결과를 못 얻을 수 있어요. 이를 막으려면 기본값을 제공하거나, 속성을 is required 트레이트로 표시해서 객체 생성 시 설정되게 해요.
class Journey {
# error if origin is not provided
has $.origin is required;
# set the destination to Orlando as default (unless that is the origin!)
has $.destination = self.origin eq 'Orlando' ?? 'Kampala' !! 'Orlando';
has @!travelers;
has $.notes is rw;
}
클래스는 Mu에서 기본 생성자를 상속하고, 접근자 메서드 생성을 요청했으므로 클래스는 이미 어느 정도 기능해요.
# Create a new instance of the class.
my $vacation = Journey.new(
origin => 'Sweden',
destination => 'Switzerland',
notes => 'Pack hiking gear!'
);
# Use an accessor; this outputs Sweden.
say $vacation.origin;
# Use a rw accessor to change the value.
$vacation.notes = 'Pack hiking gear and sunglasses!';
기본 생성자가 읽기 전용 속성을 초기화할 수는 있지만, 접근자 메서드가 있는 속성만 설정한다는 점을 주목하세요. 즉 travelers => ["Alex", "Betty"]를 기본 생성자에 넘겨도 @!travelers 속성은 초기화되지 않아요.
트레이트 (Traits)
속성의 동작을 수정하는 트레이트가 여러 개 제공돼요. 속성 트레이트는 속성 이름 뒤에 is 키워드로 도입돼요. 다음 트레이트(및 몇 가지 더)는 여기에 문서화돼 있어요.
class Journey {
# error if origin is not provided
has $.origin is required;
# reset to this default value if Nil is assigned
has $.destination is default('Orlando') is rw;
# private attribute
has @!travelers;
# is read/write
has $.notes is rw;
# is readonly
has $.agent is readonly = 'Acme Travel Inc.';
# private attribute can (only) be set via .new
has $!status is built;
}
이 새 버전의 Journey를 구성·사용하는 법은 이래요:
# Create a new instance of the class.
my $vacation = Journey.new(
origin => 'Sweden',
status => 'Requested'
);
# Use a setter accessor on a read/write attribute, then a getter to read it,
# then assign Nil to reset the default.
$vacation.destination = 'San Francisco';
say $vacation.destination; # OUTPUT: «San Francisco»
$vacation.destination = Nil;
say $vacation.destination; # OUTPUT: «Orlando»
# Try to change a built private attribute later.
try { $vacation.status = 'Booked'; } #ERROR
is readonly는 is rw로 표시되지 않은 속성의 기본값이므로 이 예시에선 효과가 없어요. 클래스 전체를 is rw로 표시할 수 있고, 그 경우 is readonly가 특정 속성을 제외(opt out)하는 데 유용해요.
메서드 (Methods)
메서드는 클래스 본문 안에서 method 키워드로 선언해요.
class Journey {
has $.origin;
has $.destination;
has @!travelers;
has $.notes is rw;
method add-traveler($name) {
if $name ne any(@!travelers) {
push @!travelers, $name;
}
else {
warn "$name is already going on the journey!";
}
}
method describe() {
"From $!origin to $!destination"
}
}
메서드는 서브루틴처럼 시그니처를 가질 수 있어요. 속성은 메서드에서 쓸 수 있고, . twigil로 선언됐더라도 항상 ! twigil로 쓸 수 있어요. . twigil이 속성을 선언하고 그 속성에 대한 접근자 메서드를 생성하기 때문이에요.
위 코드를 보면, 메서드 describe 안에서 $!origin과 $.origin을 쓰는 데는 미묘하지만 중요한 차이가 있어요. $!origin은 속성을 저렴하고 명확하게 조회하는 거예요. $.origin은 메서드 호출이라 서브클래스에서 오버라이드될 수 있어요. 오버라이드를 허용하려는 경우에만 $.origin을 쓰세요.
서브루틴과 달리, 추가적인 이름 붙은 인자는 컴파일 타임이나 런타임 오류를 만들지 않아요. 그 덕분에 재디스패치(Re-dispatching)를 통한 메서드 체이닝이 가능해요.
자동 생성된 접근자를 일부 또는 전부 오버라이드하는 직접 접근자를 쓸 수 있어요.
my $ⲧ = " " xx 4; # A tab-like thing
class Journey {
has $.origin;
has $.destination;
has @.travelers;
has Str $.notes is rw;
multi method notes() { "$!notes\n" };
multi method notes( Str $note ) { $!notes ~= "$note\n$ⲧ" };
method Str { "⤷ $!origin\n$ⲧ" ~ self.notes() ~ "$!destination ⤶\n" };
}
my $trip = Journey.new( :origin<Here>, :destination<There>,
travelers => <þor Freya> );
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
# OUTPUT:
#⤷ Here
# First steps
# Almost there
#
#There ⤶
선언된 multi 메서드 notes는 $.notes 선언에 내포된 자동 생성 메서드를 읽기·쓰기에 다른 시그니처로 오버라이드해요.
notes $trip: "Almost there"에서는 간접 인보컨트(indirect invocant) 문법을 쓰고 있어요. 메서드 이름, 객체, 콜론으로 구분된 인자 순서: method invocant: arguments예요. 이 문법은 일반적인 점·괄호 방식보다 자연스럽게 느껴질 때 쓰면 되고, 똑같이 동작해요.
Str 메서드 안에서 notes 메서드 호출이 self에 대해 이뤄지는 걸 볼 수 있어요. 이렇게 메서드 호출을 쓰면 반환값이 컨테이너에 관해 그대로 남아요. 반환값을 컨테이너화하려면 self 대신 sigil에 대해 메서드 호출을 하면 돼요. 사용한 sigil에 따라 메서드 반환값에 다양한 메서드를 호출해요:
| Sigil | Method |
|---|---|
| $ | item |
| @ | list |
| % | hash |
| & | item |
예를 들어 Journey의 Str 메서드는 반환하는 문자열에 sigil 붙은 메서드 호출을 넣어 ~ 연산자 없이 다시 쓸 수 있어요:
method Str { "⤷ $!origin\n$ⲧ$.notes()$!destination ⤶\n" }
이 섹션에서 $.notes를 갱신하는 문법은 앞의 속성 섹션과 비교해 달라졌어요. 할당:
$vacation.notes = 'Pack hiking gear and sunglasses!';
대신 이제 메서드 호출을 해요:
$trip.notes("First steps");
클래스 자동 생성 기본 접근자를 오버라이드하면 더 이상 할당을 위해 변경 가능한 컨테이너를 반환하지 않아요. 메서드 호출은 속성 갱신에 계산·로직을 추가하는 선호 방식이에요(다음 섹션에서 그런 호출의 '프로그램적 사용' 참조). 많은 현대 언어가 할당을 "setter" 메서드로 오버로드해 속성을 갱신할 수 있어요. Raku는 Proxy 객체로 이 목적에 할당 연산자를 오버로드할 수 있지만, 복잡한 로직으로 속성을 설정하도록 할당을 오버로드하는 것은 더 약한 객체 지향 설계로 여겨져 지금은 권장되지 않아요.
클래스·인스턴스 메서드 (Class and instance methods)
메서드 이름은 ."" 연산자로 런타임에 해석될 수 있어, 그러한 이름의 프로그램적 사용을 가능하게 해요. 예를 들어 속성 이름도 메서드이므로, a 메서드를 호출해서 속성 a의 값을 보여줄 수 있어요:
class A { has $.a = 9 }
my $method = 'a';
A.new."$method"().say;
# OUTPUT: «9»
인자 있는 메서드도 비슷하게 호출할 수 있어요:
class B {
has $.b = 9;
method mul($n) {
$!b * $n
}
}
my $method = 'mul';
B.new."$method"(6).say;
# OUTPUT: «54»
메서드의 시그니처는 첫 매개변수로 명시적 인보컨트를 콜론과 함께 가질 수 있고, 그렇게 하면 메서드가 호출된 객체를 참조할 수 있어요.
class Foo {
method greet($me: $person) {
say "Hi, I am $me.^name(), nice to meet you, $person";
}
}
Foo.new.greet("Bob"); # OUTPUT: «Hi, I am Foo, nice to meet you, Bob»
메서드 시그니처에 인보컨트를 제공하면 타입 제약을 통해 그 메서드를 클래스 메서드 또는 객체 메서드로 정의할 수도 있어요. ::?CLASS 변수는 컴파일 타임에 클래스 이름을 제공하는 데 쓸 수 있고, :U(다른 언어의 static과 같은 클래스 메서드)나 :D(인스턴스 메서드)와 결합해요.
class Pizza {
has $!radius = 42;
has @.ingredients;
# class method: construct from a list of ingredients
method from-ingredients(::?CLASS:U $pizza: @ingredients) {
$pizza.new( ingredients => @ingredients );
}
# instance method
method get-radius(::?CLASS:D:) { $!radius }
}
my $p = Pizza.from-ingredients: <cheese pepperoni vegetables>;
say $p.ingredients; # OUTPUT: «[cheese pepperoni vegetables]»
say $p.get-radius; # OUTPUT: «42»
say Pizza.get-radius; # This will fail.
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:
# Invocant of method 'get-radius' must be
# an object instance of type 'Pizza',
# not a type object of type 'Pizza'.
# Did you forget a '.new'?»
메서드는 multi 선언자로 클래스 메서드이자 객체 메서드가 될 수 있어요:
class C {
multi method f(::?CLASS:U:) { say "class method" }
multi method f(::?CLASS:D:) { say "object method" }
}
C.f; # OUTPUT: «class method»
C.new.f; # OUTPUT: «object method»
self
메서드 안에서 self라는 항(term)을 쓸 수 있고, 이는 인보컨트 객체에 바인딩돼요. self는 생성자를 포함해 인보컨트에 추가 메서드를 호출하는 데 쓸 수 있어요:
class Box {
has $.data;
method make-new-box-from() {
self.new: data => $!data;
}
}
self는 클래스·인스턴스 메서드 어디서든 쓸 수 있지만, 한 종류의 메서드에서 다른 종류를 호출하려는 것은 주의하세요:
class C {
method g() { 42 }
method f(::?CLASS:U:) { self.g }
method d(::?CLASS:D:) { self.f }
}
C.f; # OUTPUT: «42»
C.new.d; # This will fail.
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:
# Invocant of method 'f' must be a type object of type 'C',
# not an object instance of type 'C'. Did you forget a 'multi'?»
self는 접근자가 있는 속성에도 쓸 수 있어요. self.a는 has $.a로 선언된 속성의 접근자를 호출해요. 하지만 self.a와 $.a 사이엔 차이가 있어요. 후자는 아이템화(itemize)할 거고, $.a는 self.a.item이나 $(self.a)와 동등해요.
class A {
has Int @.numbers;
has $.x = (1, 2, 3);
method show-diff() { .say for self.x; .say for $.x }
method twice { self.times: 2 }
method thrice { $.times: 3 }
method times($val = 1) { @!numbers.map(* * $val).list }
};
my $obj = A.new(numbers => [1, 2, 3]);
$obj.show-diff; # OUTPUT: «123(1 2 3)»
say $obj.twice; # OUTPUT: «(2 4 6)»
say $obj.thrice; # OUTPUT: «(3 6 9)»
메서드 인자의 콜론 문법은 위 예시의 twice·thrice 메서드처럼 self 또는 단축형으로 메서드를 호출할 때 지원돼요.
Mu의 관련 메서드 bless, CREATE를 오버로드하지 않았다면, 그 메서드들 안에서 self는 타입 객체를 가리킬 거라는 점을 주목하세요.
반면 서브메서드 BUILD와 TWEAK는 초기화의 다른 단계에서 인스턴스에 대해 호출돼요. 서브클래스의 같은 이름 서브메서드는 아직 실행되지 않았으므로, 이 메서드 안에서 잠재적으로 가상인 메서드 호출에 의존하면 안 돼요.
Private 메서드
메서드 이름 앞에 느낌표 !가 있는 메서드는 선언하는 클래스 밖 어디에서도 호출할 수 없어요. 그런 메서드는 "private"인데, 클래스를 선언한 곳 밖에서 보이지 않는다는 뜻이에요. private 메서드는 점 대신 느낌표로 호출해요:
class FunMath {
has $.value is required;
method !do-subtraction( $num ) {
if $num ~~ Str {
return $!value + (-1 * $num.chars);
}
return $!value + (-1 * $num);
}
method minus( $minuend: $subtrahend ) {
# invoking the private method on the explicit invocant
$minuend!do-subtraction($subtrahend);
}
}
my $five = FunMath.new(value => 5);
say $five.minus(6); # OUTPUT: «-1»
say $five.do-subtraction(6);
CATCH { default { put .^name ~ ":\n" ~ .Str } }
# OUTPUT: «X::Method::NotFound:
# No such method 'do-subtraction' for invocant of type
# 'FunMath'. Did you mean '!do-subtraction'?»
private 메서드는 고유한 네임스페이스를 가져요. 가상(virtual)이 아니라서 상속하는 클래스 안에서 어떤 다형적 동작을 제공하도록 오버라이드할 수 없고, 따라서 누락된 것은 컴파일 타임에 잡혀요. 어떤 언어에서 private가 메서드의 접근성 수정자(accessibility modifier)인 것과 달리, Raku에서 "private method"와 "method"는 꽤 다른 것이에요. "private method"를 명사를 수식하는 형용사가 아니라 복합 명사로 읽는 게 낫다는 뜻이에요.
private 메서드는 서브클래스에 상속되지 않아요.
서브메서드 (Submethods)
서브메서드(submethod)는 서브클래스에 상속되지 않는 공개 메서드예요. 이름은 서브루틴과 의미상 비슷하다는 데서 유래했어요.
서브메서드는 객체 생성·소멸 작업과, 특정 타입에 너무 특화되어 하위 타입이 반드시 오버라이드해야 할 작업에 유용해요.
예를 들어 기본 메서드 new는 상속 체인의 각 클래스에서 서브메서드 BUILD를 호출해요:
class Point2D {
has $.x;
has $.y;
submethod BUILD(:$!x, :$!y) {
say "Initializing Point2D";
}
}
class InvertiblePoint2D is Point2D {
submethod BUILD() {
say "Initializing InvertiblePoint2D";
}
method invert {
self.new(x => - $.x, y => - $.y);
}
}
say InvertiblePoint2D.new(x => 1, y => 2);
# OUTPUT: «Initializing Point2D»
# OUTPUT: «Initializing InvertiblePoint2D»
# OUTPUT: «InvertiblePoint2D.new(x => 1, y => 2)»
참고: 객체 생성.
상속 (Inheritance)
클래스는 부모 클래스를 가질 수 있어요.
class Child is Parent1 is Parent2 { }
자식 클래스에서 메서드를 호출했는데 자식 클래스가 그 메서드를 제공하지 않으면, 존재한다면 부모 클래스 중 하나의 같은 이름 메서드가 대신 호출돼요. 부모 클래스를 탐색하는 순서를 *메서드 해석 순서(Method Resolution Order, MRO)*라고 해요. Raku는 C3 메서드 해석 순서를 사용해요. 메타클래스 호출로 타입의 MRO를 물어볼 수 있어요:
say List.^mro; # OUTPUT: «((List) (Cool) (Any) (Mu))»
클래스가 부모 클래스를 지정하지 않으면 기본적으로 Any가 가정돼요. 모든 클래스는 타입 계층의 루트인 Mu에서 직·간접적으로 파생돼요.
공개 메서드에 대한 모든 호출은 C++ 의미의 "가상(virtual)"이에요. 즉 어떤 메서드를 호출할지는 선언된 타입이 아니라 객체의 실제 타입이 결정해요:
class Parent {
method frob {
say "the parent class frobs"
}
}
class Child is Parent {
method frob {
say "the child's somewhat more fancy frob is called"
}
}
my Parent $test;
$test = Child.new;
$test.frob; # calls the frob method of Child rather than Parent
# OUTPUT: «the child's somewhat more fancy frob is called»
자식 객체에서 부모 메서드를 명시적으로 호출하려면 부모 네임스페이스의 전체 이름을 가리키면 돼요:
$test.Parent::frob; # calls the frob method of Parent
# OUTPUT: «the parent class frobs»
위임 (Delegation)
위임(delegation)은 위임자(delegator) 객체가 메서드 호출을 받지만 그 호출을 대신 처리할 다른 객체인 *피위임자(delegatee)*를 지정하는 기법이에요. 다시 말해, 위임자가 피위임자의 메서드 하나 이상을 자신의 것으로 공개해요.
Raku에서 위임은 속성에 handles 트레이트를 적용해 지정해요. 트레이트에 제공된 인자는 객체와 피위임자 속성이 공통으로 가질 메서드를 지정해요. 메서드 이름 목록 대신 Pair(이름 바꾸기; 키가 새 이름), Regex(이름이 매치되는 모든 메서드 처리), Whatever(속성이 호출할 수 있는 모든 메서드 위임), 또는 HyperWhatever(속성의 FALLBACK 메서드로 이어지는 것까지 모든 메서드 호출 위임)을 제공할 수 있어요. 이런 항목들을 담은 List를 줘서 여러 메서드를 위임할 수도 있어요. Regex, Whatever, HyperWhatever 형태는 클래스가 (예를 들어 Any나 Mu에서) 상속한 메서드는 위임하지 않지만, 메서드를 명시적으로 이름을 붙이면 위임해요.
class Book {
has Str $.title;
has Str $.author;
has Str $.language;
has Cool $.publication;
}
class Product {
has Book $.book handles('title', 'author', 'language', year => 'publication');
}
my $book = Book.new:
:title<Dune>,
:author('Frank Herbert'),
:language<English>,
:publication<1965>
;
given Product.new(:$book) {
say .title; # OUTPUT: «Dune»
say .author; # OUTPUT: «Frank Herbert»
say .language; # OUTPUT: «English»
say .year; # OUTPUT: «1965»
}
위 예시에서 Product 클래스는 $.book 속성을 정의하고 handles 트레이트로, Product 인스턴스 객체에서 호출될 때 Book 클래스로 전달될 메서드를 지정해요. 여기서 눈여겨볼 점이 몇 개 있어요:
Product클래스 안에서 인스턴스 객체가 호출한 메서드를 작성하지 않았어요. 대신 어떤 메서드든 그 호출을Book클래스에 위임하라고 클래스에 지시했죠.Book클래스에 나타나는 그대로 메서드 이름title,author,language를 지정했어요. 반면 적절한 Pair를 제공해서publication메서드를year로 이름을 바꿨어요.
위임은 부모 클래스에 위임하고 그 메서드 전부를 상속하지 않음으로써 상속의 대안으로 쓸 수 있어요. 예를 들어 다음 Queue 클래스는 큐에 어울리는 몇몇 메서드를 Array 클래스에 위임하면서 그중 일부(push에 대한 enqueue 같은)에 선호 인터페이스도 제공해요:
class Queue {
has @!q handles(
enqueue => 'push', dequeue => 'shift',
'push', 'shift', 'head', 'tail', 'elems', 'splice'
);
method gist {
'[' ~ @!q.join(', ') ~ ']'
}
}
my Queue $q .= new;
$q.enqueue($_) for 1..5;
$q.push(6);
say $q.shift; # OUTPUT: «1»
say $q.dequeue while $q.elems; # OUTPUT: «23456»
$q.enqueue($_) for <Perl Python Raku Ruby>;
say $q.head; # OUTPUT: «Perl»
say $q.tail; # OUTPUT: «Ruby»
say $q; # OUTPUT: «[Perl, Python, Raku, Ruby]»
$q.dequeue while $q.elems;
say $q; # OUTPUT: «[]»
객체 생성 (Object construction)
객체는 일반적으로 타입 객체 또는 같은 타입의 다른 객체에 대한 메서드 호출로 만들어져요.
클래스 Mu는 new라는 생성자 메서드를 제공하는데, 이름 붙은 인자를 받아 공개 속성을 초기화해요.
class Point {
has $.x;
has $.y;
}
my $p = Point.new( x => 5, y => 2);
# ^^^ inherited from class Mu
say "x: ", $p.x;
say "y: ", $p.y;
# OUTPUT: «x: 5»
# OUTPUT: «y: 2»
Mu.new는 인보컨트에 bless 메서드를 호출하며 모든 이름 붙은 인자를 넘겨요. bless는 새 객체를 만들고, 그 다음 모든 서브클래스를 역순 MRO(즉 Mu에서 가장 파생된 클래스까지)로 훑어요. 각 클래스에서 bless는 다음 단계를 나열된 순서대로 실행해요:
BUILD라는 이름의 메서드 존재를 검사해요. 존재하면 (new메서드에서 받은) 모든 이름 붙은 인자로 그 메서드를 호출해요.BUILD메서드가 없으면 이 클래스의 공개 속성을 같은 이름의 이름 붙은 인자로 초기화해요.- 앞 단계에서 건드리지 않은 모든 속성에 기본값을 적용해요:
has $.attribute = 'default value'; TWEAK이 존재하면 호출해요.BUILD가 받는 것과 같은 인자를 받아요.
이 객체 생성 체계는 몇 가지 함의를 가져요:
- (Mu에서 상속한) 기본
new생성자에 대한 이름 붙은 인자는 MRO의 어떤 클래스의 공개 속성에 직접 대응하거나, 어떤BUILD·TWEAK서브메서드의 이름 붙은 매개변수에 대응할 수 있어요. - 맞춤
BUILD메서드는 항상 서브메서드여야 해요. 그렇지 않으면 서브클래스에 상속되어, 서브클래스가 자기BUILD가 없을 때 기본 속성 초기화(위 목록의 두 번째 항목)를 막아요. BUILD는 속성을 설정할 수 있지만, 기본값으로 선언된 속성의 내용에는 접근할 수 없어요. 그 값은 나중에만 적용되기 때문이죠. 반면TWEAK은 기본값 적용 후 호출되어 속성이 초기화된 상태를 찾아요. 그래서 객체 생성 후 뭔가를 검사하거나 속성을 수정하는 데 쓸 수 있어요.
class RectangleWithCachedArea {
has ($.x1, $.x2, $.y1, $.y2);
has $.area;
submethod TWEAK() {
$!area = abs( ($!x2 - $!x1) * ( $!y2 - $!y1) );
}
}
say RectangleWithCachedArea.new( x2 => 5, x1 => 1, y2 => 1, y1 => 0).area;
# OUTPUT: «4»
- 루틴에 인자를 넘기면 인자가 매개변수에 바인딩되므로, 속성을 매개변수로 써서 BUILD 메서드를 단순화할 수 있어요.
BUILD메서드에서 일반 바인딩을 쓰는 클래스:
class Point {
has $.x;
has $.y;
submethod BUILD(:$x, :$y) {
$!x := $x;
$!y := $y;
}
}
my $p1 = Point.new( x => 10, y => 5 );
다음 BUILD 메서드는 위와 동등해요:
submethod BUILD(:$!x, :$!y) {
# Nothing to do here anymore, the signature binding
# does all the work for us.
}
BUILD()메서드와 함께 기본값을 쓰려면 속성의 매개변수 바인딩을 쓸 수 없어요. 항상 속성을 건드려 기본값 자동 할당(위 목록의 세 번째 단계)을 막기 때문이에요. 대신 값을 조건부로 할당해야 해요:
class A {
has $.attr = 'default';
submethod BUILD(:$attr) {
$!attr = $attr if defined $attr;
}
}
say A.new(attr => 'passed').raku;
say A.new().raku;
# OUTPUT: «A.new(attr => "passed")»
# OUTPUT: «A.new(attr => "default")»
하지만 BUILD 매개변수의 기본값을 설정하는 게 더 간단해요:
class A {
has $.attr;
submethod BUILD(:$!attr = 'default') {}
}
- 속성이 Int 같은 특별한 타입 요구사항이 있을 때 속성의 매개변수 바인딩을 쓰는 건 조심해야 해요. 이 매개변수 없이
new를 호출하면 Any 기본값이 할당되어 타입 오류가 나요. 쉬운 수정은BUILD매개변수에 기본값을 추가하는 거예요.
class A {
has Int $.attr;
submethod BUILD(:$!attr = 0) {}
}
say A.new(attr => 1).raku;
say A.new().raku;
# OUTPUT: «A.new(attr => 1)»
# OUTPUT: «A.new(attr => 0)»
BUILD는 속성 초기화의 별명을 만들 수 있어요:
class EncodedBuffer {
has $.enc;
has $.data;
submethod BUILD(:encoding(:$!enc), :$!data) { }
}
my $b1 = EncodedBuffer.new( encoding => 'UTF-8', data => [64, 65] );
my $b2 = EncodedBuffer.new( enc => 'UTF-8', data => [64, 65] );
# both enc and encoding are allowed now
new라는 이름은 Raku에서 특별하지 않다는 점을 주목하세요. 대부분의 Raku 클래스에서 아주 꼼꼼히 지켜지는 흔한 관례일 뿐이에요. 아무 메서드에서나bless를 호출하거나CREATE를 써서 저수준 작업을 다룰 수 있어요.- 위치 인자를 받는 생성자를 원한다면 직접
new메서드를 써야 해요:
class Point {
has $.x;
has $.y;
method new($x, $y) {
self.bless(:$x, :$y);
}
}
다만 new는 일반 메서드이고 bless의 생성 과정에는 관여하지 않는다는 걸 주의하세요. 그래서 new 메서드에 넣은 로직은 다른 new 메서드나 서브클래스의 new를 쓸 때 호출되지 않아요.
class Vector {
has $.x;
has $.y;
has $.length;
method new($x, $y) {
self.bless(:$x, :$y, length => sqrt($x**2 * $y**2));
}
}
class NamedVector is Vector {
has $.name;
method new($name, $x, $y) {
self.bless(:$name, :$x, :$y);
}
}
my $v = Vector.new: 3, 4;
say $v.length; # OUTPUT: «5»
my $f = NamedVector.new: 'Francis', 5, 12;
say $f.length; # OUTPUT: «(Any)»
여기 Str 클래스를 자동 증가하는 ID로 풍부하게 만드는 예시예요:
class Str-with-ID is Str {
my $counter = 0;
has Int $.ID is rw = 0;
multi method new( $str ) {
self.bless( value => $str );
}
submethod BUILD( :$!ID = $counter++ ) {}
}
say Str-with-ID.new("1.1,2e2").ID; # OUTPUT: «0»
my $enriched-str = Str-with-ID.new("3,4");
say "$enriched-str, {$enriched-str.^name}, {$enriched-str.ID}";
# OUTPUT: «3,4, Str-with-ID, 1»
새 클래스를 맨 문자열로 초기화하고 싶어서 맞춤 new를 만들었어요. bless는 Str.BUILD를 호출할 거고, 그것이 찾는 값 value => $str 페어를 잡아 자신을 초기화해요. 하지만 서브클래스의 속성도 초기화해야 하므로 BUILD 안에서 $.ID를 초기화해요. 출력에서 보듯 객체는 ID로 올바르게 초기화되고 일반 Str처럼 쓸 수 있어요.
객체 복제 (Object cloning)
복제는 모든 객체에서 쓸 수 있는 clone 메서드로 이뤄지며, 공개·private 속성을 둘 다 얕게 복제해요. 공개 속성의 새 값은 이름 붙은 인자로 제공할 수 있어요.
class Foo {
has $.foo = 42;
has $.bar = 100;
}
my $o1 = Foo.new;
my $o2 = $o1.clone: :bar(5000);
say $o1; # OUTPUT: «Foo.new(foo => 42, bar => 100)»
say $o2; # OUTPUT: «Foo.new(foo => 42, bar => 5000)»
비스칼라 속성이 어떻게 복제되는지와 직접 맞춤 clone 메서드를 구현하는 예시에 대한 자세한 내용은 clone 문서를 보세요.
Roles (역할)
Role은 속성과 메서드의 모음이에요. 하지만 클래스와 달리 role은 객체 동작의 일부만 설명하기 위한 거예요. 그래서 일반적으로 role은 클래스와 객체에 *믹스인(mix in)*되도록 고안됐어요. 일반적으로 클래스는 객체 관리용이고, role은 객체 안의 동작·코드 재사용 관리용이에요.
Role은 선언될 role 이름 앞에 role 키워드를 써요. Role은 믹스인할 role 이름 앞에 does 키워드를 써서 믹스인해요.
Role은 is를 써서 클래스에 믹스인할 수도 있어요. 하지만 role과 함께 is의 의미는 does가 제공하는 것과 꽤 달라요. is로는 role에서 클래스를 pun한 다음 상속해요. 그래서 평탄화 합성(flattening composition)이 없고, does가 제공하는 안전장치가 하나도 없어요.
constant ⲧ = " " xx 4; #Just a ⲧab
role Notable {
has Str $.notes is rw;
multi method notes() { "$!notes\n" };
multi method notes( Str $note ) { $!notes ~= "$note\n" ~ ⲧ };
}
class Journey does Notable {
has $.origin;
has $.destination;
has @.travelers;
method Str { "⤷ $!origin\n" ~ ⲧ ~ self.notes() ~ "$!destination ⤶\n" };
}
my $trip = Journey.new( :origin<Here>, :destination<There>,
travelers => <þor Freya> );
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
# OUTPUT:
#⤷ Here
# First steps
# Almost there
#
#There ⤶
Role은 컴파일러가 role 선언의 닫는 중괄호를 파싱하는 즉시 불변(immutable)이 돼요.
Role 적용 (Applying roles)
Role 적용은 클래스 상속과 크게 달라요. role을 클래스에 적용하면 그 role의 메서드가 클래스로 복사돼요. 여러 role을 같은 클래스에 적용하면 충돌(예: 같은 이름의 속성이나 non-multi 메서드)은 컴파일 타임 오류를 만들고, 클래스에 같은 이름의 메서드를 제공해서 해결할 수 있어요.
이것은 다중 상속보다 훨씬 안전해요. 다중 상속에선 컴파일러가 충돌을 절대 감지하지 못하고, 대신 프로그래머가 원하지 않을 수도 있는 MRO에서 먼저 나타나는 슈퍼클래스로 해결하니까요.
예를 들어 소를 타고 가는 효율적인 방법을 발견해서 이것을 인기 있는 새 교통수단으로 마케팅하려 한다고 해요. 집에 두는 모든 황소를 위한 Bull 클래스와, 운전할 수 있는 것을 위한 Automobile 클래스가 있을 거예요.
class Bull {
has Bool $.castrated = False;
method steer {
# Turn your bull into a steer
$!castrated = True;
return self;
}
}
class Automobile {
has $.direction;
method steer($!direction) { }
}
class Taurus is Bull is Automobile { }
my $t = Taurus.new;
say $t.steer;
# OUTPUT: «Taurus.new(castrated => Bool::True, direction => Any)»
이렇게 설정하면 불쌍한 고객들은 자기 Taurus를 조향할 수 없고, 당신은 제품을 더 만들 수 없어져요! 이런 경우 role을 쓰는 편이 나았을 거예요:
role Bull-Like {
has Bool $.castrated = False;
method steer {
# Turn your bull into a steer
$!castrated = True;
return self;
}
}
role Steerable {
has Real $.direction;
method steer(Real $d = 0) {
$!direction += $d;
}
}
class Taurus does Bull-Like does Steerable { }
이 코드는 이렇게 비슷한 걸로 죽을 거예요:
===SORRY!===
Method 'steer' must be resolved by class Taurus because it exists in
multiple roles (Steerable, Bull-Like)
이 검사는 많은 골칫거리를 덜어줘요:
class Taurus does Bull-Like does Steerable {
method steer($direction?) {
self.Steerable::steer($direction)
}
}
role이 두 번째 role에 적용되면 실제 적용은 두 번째 role이 클래스에 적용될 때까지 지연돼요. 그 시점에 두 role이 모두 클래스에 적용되죠. 그래서
role R1 {
# methods here
}
role R2 does R1 {
# methods here
}
class C does R2 { }
는 다음과 같은 같은 클래스 C를 만들어요:
role R1 {
# methods here
}
role R2 {
# methods here
}
class C does R1 does R2 { }
스텁 (Stubs)
role이 스텁 메서드 — 코드가 ...로 제한된 메서드 — 를 포함하면, role을 클래스에 적용할 때 같은 이름의 non-stub 메서드를 제공해야 해요. 이 덕분에 추상 인터페이스처럼 동작하는 role을 만들 수 있어요.
role AbstractSerializable {
method serialize() { ... } # literal ... here marks the
# method as a stub
}
# the following is a compile time error, for example
# Method 'serialize' must be implemented by Point because
# it's required by a role
class APoint does AbstractSerializable {
has $.x;
has $.y;
}
# this works:
class SPoint does AbstractSerializable {
has $.x;
has $.y;
method serialize() { "p($.x, $.y)" }
}
스텁 메서드의 구현은 다른 role이 제공할 수도 있어요.
상속 (Inheritance)
Role은 클래스에서 상속할 수 없지만, 클래스를 지닐(carry) 수는 있어서 그 role을 하는 어떤 클래스든 지니어진 클래스에서 상속하게 돼요. 그래서 다음과 같이 쓰면:
role A is Exception { }
class X::Ouch does A { }
X::Ouch.^parents.say # OUTPUT: «((Exception))»
X::Ouch는 위에서 부모를 나열해 보듯 Exception에서 직접 상속해요.
Role은 제대로 된 상속을 쓰지 않으므로 클래스 계층의 일부가 아니에요. Role은 그 대신 .^roles 메타메서드로 나열되는데, 모든 단계를 포함할지 첫 단계만 포함할지 transitive 플래그를 써요. 그럼에도 클래스나 인스턴스는 role을 하는지 스마트매치나 타입 제약으로 검사할 수 있어요.
role F { }
class G does F { }
G.^roles.say; # OUTPUT: «((F))»
role Ur {}
role Ar does Ur {}
class Whim does Ar {}; Whim.^roles(:!transitive).say; # OUTPUT: «((Ar))»
say G ~~ F; # OUTPUT: «True»
multi a (F $a) { "F".say }
multi a ($a) { "not F".say }
a(G); # OUTPUT: «F»
쪼는 순서 (Pecking order)
클래스에 직접 정의된 메서드는 항상 적용된 role이나 상속된 클래스의 정의를 오버라이드해요. 그런 정의가 없으면 role의 메서드가 클래스에서 상속된 메서드를 오버라이드해요. 이는 해당 클래스가 role에 의해 들어왔을 때와 직접 상속됐을 때 모두 그래요.
role M {
method f { say "I am in role M" }
}
class A {
method f { say "I am in class A" }
}
class B is A does M {
method f { say "I am in class B" }
}
class C is A does M { }
B.new.f; # OUTPUT: «I am in class B»
C.new.f; # OUTPUT: «I am in role M»
multi 메서드의 각 후보는 그 자체로 하나의 메서드라는 점을 주목하세요. 즉 위 규칙은 두 후보가 같은 시그니처일 때만 적용돼요. 그렇지 않으면 충돌이 없고, 후보는 그냥 multi 메서드에 추가돼요.
자동 role punning
role을 직접 인스턴스화하거나 타입 객체로 쓰려는 어떤 시도든 role과 같은 이름의 클래스를 자동으로 만들어, role을 클래스처럼 투명하게 쓸 수 있게 해요.
role Point {
has $.x;
has $.y;
method abs { sqrt($.x * $.x + $.y * $.y) }
method dimensions { 2 }
}
say Point.new(x => 6, y => 8).abs; # OUTPUT: «10»
say Point.dimensions; # OUTPUT: «2»
이런 클래스 자동 생성을 punning이라고 하고, 만들어진 클래스를 pun이라고 불러요.
하지만 메타프로그래밍 구조 대부분은 role과 직접 작업할 때 쓰이므로, punning은 대부분의 메타프로그래밍 구조에 의해 발생하지 않아요.
매개변수화된 role (Parameterized roles)
Role은 대괄호 안에 시그니처를 줘서 매개변수화할 수 있어요:
role BinaryTree[::Type] {
has BinaryTree[Type] $.left;
has BinaryTree[Type] $.right;
has Type $.node;
method visit-preorder(&cb) {
cb $.node;
for $.left, $.right -> $branch {
$branch.visit-preorder(&cb) if defined $branch;
}
}
method visit-postorder(&cb) {
for $.left, $.right -> $branch {
$branch.visit-postorder(&cb) if defined $branch;
}
cb $.node;
}
method new-from-list(::?CLASS:U: *@el) {
my $middle-index = @el.elems div 2;
my @left = @el[0 .. $middle-index - 1];
my $middle = @el[$middle-index];
my @right = @el[$middle-index + 1 .. *];
self.new(
node => $middle,
left => @left ?? self.new-from-list(@left) !! self,
right => @right ?? self.new-from-list(@right) !! self,
);
}
}
my $t = BinaryTree[Int].new-from-list(4, 5, 6);
$t.visit-preorder(&say); # OUTPUT: «546»
$t.visit-postorder(&say); # OUTPUT: «465»
여기 시그니처는 타입 캡처로만 이뤄져 있지만, 어떤 시그니처든 돼요:
enum Severity <debug info warn error critical>;
role Logging[$filehandle = $*ERR] {
method log(Severity $sev, $message) {
$filehandle.print("[{uc $sev}] $message\n");
}
}
Logging[$*OUT].log(debug, 'here we go'); # OUTPUT: «[DEBUG] here we go»
같은 이름이지만 다른 시그니처를 가진 role을 여러 개 가질 수 있어요. multi 후보를 고를 때 일반 multi 디스패치 규칙이 적용돼요.
믹스인 (Mixins)
Role은 객체에 믹스인할 수 있어요. role의 속성·메서드는 객체가 이미 가진 메서드·속성에 추가돼요. 여러 믹스인과 익명 role이 지원돼요.
role R { method Str() {'hidden!'} };
my $i = 2 but R;
sub f(\bound){ put bound };
f($i); # OUTPUT: «hidden!»
my @positional := <a b> but R;
say @positional.^name; # OUTPUT: «List+{R}»
객체가 (객체의 클래스나 컨테이너가 아니라) role을 믹스인했다는 점을 주목하세요. 그래서 @-sigil 컨테이너는 role이 붙도록 바인딩이 필요해요. @positional 예시에서 보듯이요. 일부 연산자는 role을 결과에서 벗겨내는 새 값을 반환해요. 그래서 does를 써서 변수 선언에 role을 믹스인하는 게 더 명확할 수 있어요:
role R {};
my @positional does R = <a b>;
say @positional.^name; # OUTPUT: «Array+{R}»
연산자 infix:<but>는 리스트 생성자보다 좁아요. 믹스인할 role 목록을 줄 때는 항상 괄호를 사용하세요.
role R1 { method m {} }
role R2 { method n {} }
my $a = 1 but R1,R2; # R2 is in sink context, issues a WARNING
say $a.^name;
# OUTPUT: «Int+{R1}»
my $all-roles = 1 but (R1,R2);
say $all-roles.^name; # OUTPUT: «Int+{R1,R2}»
role이 정확히 하나의 속성을 제공하면 괄호 안에 초기화자를 전달할 수 있어요:
role Named {
has $.name;
}
my $hero = 1.Rat but Named('Remy');
say $hero.name; # OUTPUT: «Remy»
믹스인은 객체 수명의 어느 시점에서든 쓸 수 있어요.
# A counter for Table of Contents
role TOC-Counter {
has Int @!counters is default(0);
method Str() { @!counters.join: '.' }
method inc($level) {
@!counters[$level - 1]++;
@!counters.splice($level);
self
}
}
my Num $toc-counter = NaN; # don't do math with Not A Number
say $toc-counter; # OUTPUT: «NaN»
$toc-counter does TOC-Counter; # now we mix the role in
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1; # OUTPUT: «NaN» (because that's numerical context)
put $toc-counter; # OUTPUT: «2.2.2» (put will call TOC-Counter::Str)
Role은 익명일 수 있어요.
my %seen of Int is default(0 but role :: { method Str() {'NULL'} });
say %seen<not-there>; # OUTPUT: «NULL»
say %seen<not-there>.defined; # OUTPUT: «True» (0 may be False but is well defined)
say Int.new(%seen<not-there>); # OUTPUT: «0»
메타객체 프로그래밍과 내부 들여다보기 (Metaobject programming and introspection)
Raku에는 메타객체 시스템이 있어서, 객체·클래스·role·문법·enum 등의 동작 자체가 다른 객체에 의해 제어돼요. 그런 객체를 *메타객체(metaobject)*라고 불러요. 메타객체는 일반 객체처럼 클래스의 인스턴스인데, 그 클래스를 *메타클래스(metaclass)*라고 해요.
각 객체·클래스에 대해 .HOW를 호출해서 메타객체를 얻을 수 있어요. 메서드 호출처럼 보이지만 매크로처럼 동작해요.
그래서 메타객체로 무엇을 할 수 있을까요? 우선 두 객체가 같은 메타클래스를 갖는지 동등성 비교로 확인할 수 있어요:
say 1.HOW === 2.HOW; # OUTPUT: «True»
say 1.HOW === Int.HOW; # OUTPUT: «True»
say 1.HOW === Num.HOW; # OUTPUT: «False»
Raku는 메타객체 시스템을 가리켜 HOW(Higher Order Workings)라는 말을 써요. 그래서 Rakudo에서 클래스 동작을 제어하는 메타클래스의 클래스 이름이 Perl6::Metamodel::ClassHOW라는 건 놀랍지 않아요. 각 클래스마다 Perl6::Metamodel::ClassHOW 인스턴스가 하나씩 있어요.
하지만 메타모델은 훨씬 더 많은 걸 해줘요. 예를 들어 객체·클래스를 들여다볼 수 있게 해주죠. 메타객체의 메서드 호출 규약은 메타객체에 메서드를 호출하고 관심 객체를 첫 인자로 넘기는 거예요. 그래서 객체의 클래스 이름을 얻으려면 이렇게 쓸 수 있어요:
my $object = 1;
my $metaobject = 1.HOW;
say $metaobject.name($object); # OUTPUT: «Int»
# or shorter:
say 1.HOW.name(1); # OUTPUT: «Int»
(동기는 Raku가 타입마다 새 메타객체를 만들 필요가 없는 더 프로토타입 기반 객체 시스템도 허용하고 싶기 때문이에요.)
같은 객체를 두 번 쓰지 않게 하는 단축이 있어요:
say 1.^name; # OUTPUT: «Int»
# same as
say 1.HOW.name(1); # OUTPUT: «Int»
class의 메타클래스에 대한 문서는 Metamodel::ClassHOW를, 메타객체 프로토콜에 대한 일반 문서는 MOP를 보세요.