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'"␤»
}

출처: Raku 공식 문서 — Attribute