Raku의 Attribute — 객체의 저장 슬롯을 메타 수준에서 다루기
Raku의 Attribute — 객체의 저장 슬롯을 메타 수준에서 다루기
클래스 안의 has $.a 같은 선언은 결국 객체마다 하나씩 값을 담는 저장 슬롯(slot)을 만드는 거예요. Raku에서는 이 슬롯 하나하나를 attribute(속성)라고 부르고, Attribute 클래스는 클래스·역할의 속성을 메타 수준(Meta Object Protocol)에서 다룰 때 사용해요.
class Attribute { }
본문
Attribute는 주로 MOP 작업에서 유용해요. 예를 들어 어떤 타입의 속성들을 들여다보려면 attributes 메타메서드를 쓰면 돼요. 그 결과로 Attribute 인스턴스들의 리스트가 돌아오고, 이를 통해 속성의 이름 같은 여러 성질을 확인할 수 있어요.
class WithAttributes {
has $.attribute;
has $.attribute-two-electric-boogaloo;
has $.yet-another-attribute;
}
.say for WithAttributes.^attributes(:local).map(*.name);
# OUTPUT:
# $!attribute
# $!attribute-two-electric-boogaloo
# $!yet-another-attribute
속성을 가진 타입이 컴파일러만이 만들어낼 수 있는 건 아니에요. Attribute 덕분에 메타 수준에서 직접 생성할 수도 있어요.
class WithAttribute {
has $.attribute;
}
위 클래스는 아래처럼 수동으로 만들 수도 있어요.
BEGIN {
constant WithAttribute = Metamodel::ClassHOW.new_type: :name<WithAttribute>;
WithAttribute.^add_attribute: Attribute.new:
:name<$!attribute>, :type(Any), :package(WithAttribute),
:1has_accessor;
WithAttribute.^compose;
}
트레이트 (Traits)
trait is default
Nil이 할당된 속성은 is default 트레이트로 설정된 기본값으로 되돌아가요. 배열이나 연관 구조의 경우 is default의 인자가 기본 항목 값 또는 해시 값을 설정해요.
class C {
has $.a is default(42) is rw = 666
}
my $c = C.new;
say $c;
$c.a = Nil;
say $c;
# OUTPUT: «C.new(a => 666)C.new(a => 42)»
class Foo {
has @.bar is default(42) is rw
};
my $foo = Foo.new( bar => <a b c> );
$foo.bar =Nil;
say $foo; # OUTPUT: «Foo.new(bar => [42])»
trait is required
multi trait_mod:<is> (Attribute $attr, :$required!)
is required 트레이트는 객체가 인스턴스화될 때 반드시 값을 채워야 하는 속성으로 표시해요. 값을 주지 않으면 런타임 오류가 발생해요.
class C {
has $.a is required
}
my $c = C.new;
CATCH{ default { say .^name, ': ', .Str } }
# OUTPUT: «X::Attribute::Required: The attribute '$!a' is required, but you did not provide a value for it.»
이 트레이트 덕분에 :D 스마일리를 가진 타입으로 속성을 선언하면서도 기본값을 주지 않을 수 있어요.
class Power {
has Numeric:D $.base is required;
has Numeric:D $.exponent is required;
multi method Numeric(::?CLASS:D: --> Numeric:D) {
$!base ** $!exponent
}
}
6.d 언어 버전부터(초기 구현은 Rakudo 2018.08+) 왜 이 속성이 필요한지 이유 문자열을 지정할 수도 있어요.
class D {
has $.a is required("it is a good idea");
}
my $d = D.new;
CATCH{ default { say .^name, ': ', .Str } }
# OUTPUT: «X::Attribute::Required: The attribute '$!a' is required because it is a good idea,but you did not provide a value for it.»
is required는 기본 생성자에만 영향을 주는 게 아니에요. 더 낮은 수준에서 속성을 검사하기 때문에, bless로 직접 만든 커스텀 생성자에서도 동작해요.
trait is DEPRECATED
multi trait_mod:<is>(Attribute:D $r, :$DEPRECATED!)
속성을 deprecated(사용 중단)로 표시하고, 선택적으로 대신 쓸 것을 알려주는 메시지를 달아요.
class C {
has $.foo is DEPRECATED("'bar'");
}
my $c = C.new( foo => 42 ); # doesn't trigger with initialization (yet)
say $c.foo; # does trigger on usage
프로그램이 끝난 뒤 STDERR에 아래 같은 내용이 표시돼요.
# Saw 1 occurrence of deprecated code.
# =====================================
# Method foo (from C) seen at:
# script.raku, line 5
# Please use 'bar' instead.
trait is rw
multi trait_mod:<is> (Attribute:D $attr, :$rw!)
기본이 읽기 전용인 것과 달리, 속성을 읽기/쓰기로 표시해요. 접근자가 쓰기 가능한 값을 돌려줘요.
class Boo {
has $.bar is rw;
has $.baz;
};
my $boo = Boo.new;
$boo.bar = 42; # works
$boo.baz = 42;
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Any»
trait is readonly
multi trait_mod:<is> (Attribute:D $attr, :readonly($)!)
is rw 트레이트가 붙은 클래스의 속성을 readonly로 다시 표시해요.
class Thing is rw {
has $.form;
has $.matter is readonly;
};
my $t = Thing.new(matter => "copper", form => "cubic");
$t.form = "round"; # OK, form is rw
$t.matter = "iron"; # not OK, matter is readonly
# OUTPUT: «Cannot modify an immutable Str (copper)...»
is rw 트레이트가 없는 클래스의 속성은 이미 기본적으로 읽기 전용이에요. 그런 클래스에서는 is readonly가 사실 중복이에요.
trait is built
multi trait_mod:<is>(Attribute:D $a, :$built!)
기본적으로 이 트레이트는 .new를 통한 객체 생성 동안 private 속성도 설정할 수 있게 해줘요. 반대로 public 속성에 built(False)를 주면 .new로 설정하지 못하게 막을 수 있어요. 값 설정은 보통 할당으로 처리되는데, :bind를 넘기면 바인딩으로 처리돼요.
class Foo {
has $!bar is built; # same as `is built(True)`
has $.baz is built(False);
has $!qux is built(:bind);
method bar(::?CLASS:D:) { $!bar }
method qux(::?CLASS:D:) { $!qux }
}
my Foo:D $foo .= new: :bar[], :baz[], :qux[];
say $foo.bar.raku; # OUTPUT: «$[]»
say $foo.baz.raku; # OUTPUT: «Any»
say $foo.qux.raku; # OUTPUT: «[]»
:bind를 쓰면 (기본)값이 할당이 아니라 속성에 바인딩돼요. 이렇게 하면 Proxy를 속성에 지정할 수 있어요.
class Foo {
has $!foo is built(:bind) = Proxy.new: :STORE{...}, :FETCH{...}
}
Rakudo 컴파일러 2020.01 릴리스부터 사용 가능해요.
메서드
method new
method new(
Attribute:_:
:$name!,
:$type!,
:$package!,
:$inlined = 0,
:$has_accessor = 0,
:$is_built = $has_accessor,
:$is_bound = 0,
:$positional_delegate = 0,
:$associative_delegate = 0,
*%other
)
새 속성을 만들어요. $name(속성의 이름), $type, $package는 필수 named 인자예요.
method name
method name(Attribute:D: --> Str:D)
속성의 이름을 돌려줘요. 항상 private 이름을 돌려주므로, has $.a로 선언했다면 $!a가 돌아와요.
class Foo {
has @!bar;
}
my $a = Foo.^attributes(:local)[0];
say $a.name; # OUTPUT: «@!bar»
method package
method package()
이 속성이 속한 패키지(클래스/문법/역할)를 돌려줘요.
method has_accessor
method has_accessor(Attribute:D: --> Bool:D)
속성이 public 접근자 메서드를 가지면 True를 돌려줘요.
method rw
method rw(Attribute:D: --> Bool:D)
is rw 트레이트가 적용된 속성이면 True를 돌려줘요.
method readonly
method readonly(Attribute:D: --> Bool:D)
읽기 전용 속성(기본값)이면 True, is rw로 표시된 속성이면 False를 돌려줘요.
method required
method required(Attribute:D: --> Any:D)
is required 트레이트가 적용된 속성이면 1을, 적용되지 않았으면 Mu를 돌려줘요. 문자열과 함께 적용했다면 그 문자열을 돌려줘요.
method type
method type(Attribute:D: --> Mu)
속성의 타입 제약을 돌려줘요.
method get_value
method get_value(Mu $obj)
$obj 객체의 이 속성에 저장된 값을 돌려줘요. 캡슐화를 깨뜨리므로 주의해서 써야 해요.
method set_value
method set_value(Mu $obj, Mu \new_val)
$obj 객체의 이 속성에 new_val 값을 바인딩해요. 역시 캡슐화를 위반하니 주의가 필요해요. "여기 용이 있다(Here be dragons)"는 경고를 붙인다고 보면 돼요.
method gist
multi method gist(Attribute:D:)
타입의 이름에 이어 속성의 이름을 돌려줘요.
class Hero {
has @!inventory;
has Str $.name;
submethod BUILD( :$name, :@inventory ) {
$!name = $name;
@!inventory = @inventory
}
}
say Hero.^attributes(:local)[0]; # OUTPUT: «Positional @!inventory»
say는 내부에서 .gist를 호출하므로 그 결과가 출력돼요.
method is_built
method is_built()
is built 트레이트가 적용된 속성이면 True, 아니면 False를 돌려줘요.
method is_bound
method is_bound()
is built 트레이트가 :bind 인자와 함께 적용된 속성이면 True, 아니면 False를 돌려줘요.
선택적 인트로스펙션
DEPRECATED
속성이 DEPRECATED로 표시되어 있으면 DEPRECATED 메서드를 호출할 수 있어요. 특정 이유가 지정되지 않았다면 "something else"를, 지정했다면 그 문자열을 돌려줘요. 표시되어 있지 않으면 호출할 수 없으니 .? 메서드 문법을 써야 해요.
class Hangout {
has $.table;
has $.bar is DEPRECATED("the patio");
}
my $attr-table = Hangout.^attributes(:local)[0];
my $attr-bar = Hangout.^attributes(:local)[1];
with $attr-table.?DEPRECATED -> $text { # does not trigger
say "Table is deprecated with '$text'";
# OUTPUT:
}
with $attr-bar.?DEPRECATED -> $text {
say "Bar is deprecated with '$text'";
# OUTPUT: «Bar is deprecated with 'the patio'"»
}