스토어 프록시 페치
Source: Dev.to
죄송합니다만, 번역을 진행하려면 실제 텍스트 내용이 필요합니다. 해당 글의 본문을 복사해서 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다.
Source: …
UPPER 사례 – 파트 12
Raku의 컨테이너
이 글은 UPPERCASE 로 완전히 작성된 Raku 구문 요소들을 문서화하는 Cases of UPPER 시리즈의 12번째 파트입니다.
이번 포스트에서는 실제로 Raku에서 값을 보관하는 객체인 컨테이너에 대해 살펴봅니다.
바인딩 vs. 할당
Raku는 실제로 바인딩(:=)만을 알고 있습니다.
할당(=)은 결국 바인딩을 수행하는 문법 설탕에 불과합니다.
뭐라고?
Raku에서 보는 대부분의 스칼라 변수는 값이 할당될 때 해당 값이 바인딩되는 속성을 가진 객체입니다. 이러한 객체를 Scalar 컨테이너(또는 간단히 컨테이너)라고 부릅니다.
요약하면: Raku에서 할당은 Scalar 객체의 속성에 바인딩하는 행위입니다.
컨테이너를 이해하는 것은 효율적인 Raku 개발자가 되고자 하는 모든 사람에게 꼭 필요한 통과 의식입니다 – 저에게도 마찬가지였죠!
(아주) 단순화된 모델
# Pseudocode – not real Raku
class Scalar {
has $!value;
method STORE($new-value) {
$!value := $new-value;
}
method FETCH() {
$!value;
}
}
다음과 같은 할당을
my $a;
$a = 42;
다음과 같이 생각할 수 있습니다.
my $a := Scalar.new; # $a를 새로운 컨테이너에 바인드
$a.STORE(42); # 값을 저장
그리고 변수를 출력(say $a)하는 것은 다음과 동일합니다.
say $a.FETCH;
실제로는 다음과 같은 이유로 더 복잡합니다.
- 형 제한(
my Int $a)이 변수에 붙을 수 있습니다. - 변수는 기본값(
my $a is default(42))을 가질 수 있습니다.
이 모든 추가 정보는 컨테이너 디스크립터에 저장됩니다 – 직접 의존해서는 안 되는 구현 세부 사항이며, 인터페이스가 바뀔 수 있습니다.
왜 간접 참조가 필요할까?
할당은 가장 자주 실행되는 연산 중 하나이므로, Raku는 성능을 위해 많은 최적화를 적용합니다.
컨테이너에 대한 정신 모델을 갖추면 언어 구조를 놀라울 정도로 많이 이해할 수 있습니다.
컨테이너 반환 ( rw 트레이트)
블록(또는 return)에서 반환되는 값은 기본적으로 컨테이너가 해제된 형태입니다.
my $a = 42;
sub foo() { $a } # 값 42를 반환
foo() = 666; # ❌ 불변 Int에 할당 불가
컨테이너 자체를 반환하고 싶다면 두 가지 간단한 방법이 있습니다.
1. is rw 트레이트
my $a = 42;
sub foo() is rw { $a } # 컨테이너 반환
foo() = 666; # 동작함
say $a; # 666
2. return-rw 문
my $a = 42;
sub foo() { return-rw $a }
foo() = 666;
say $a; # 666
이 메커니즘은 예를 들어 배열 요소에 할당할 때 사용됩니다.
코어에서는 속도 향상을 위해 후위 연산자 […]와 기본 AT-POS 메서드 모두 is rw 트레이트를 가지고 있으며, return-rw 문은 약간의 오버헤드만 추가합니다.
배열과 해시의 컨테이너
배열을 값으로 초기화하면 각 원소마다 별도의 컨테이너가 생성됩니다.
값을 fetch 하거나, 새 값을 store 하거나, 컨테이너를 다른 변수에 bind 할 수 있습니다.
my @a = 1, 2, 3, 4, 5;
$_++ for @a; # 각 원소를 증가
say @a; # [2 3 4 5 6]
- 루프 동안 토픽 변수
$_는 각 원소의 컨테이너에 바인드되므로$_++는 실제로 해당 컨테이너의STORE를 호출합니다.
주의:
$_는 메모리 위치에 대한 포인터나 레퍼런스가 아닙니다.FETCH와STORE를 수행할 수 있는 컨테이너 객체에 바인드된 것입니다.
슬라이스도 컨테이너를 반환한다
my @a = 1, 2, 3, 4, 5;
$_++ for @a[0, *-1]; # 첫 번째와 마지막 원소만
say @a; # [2 2 3 4 6]
슬라이스 [0, *-1]는 두 개의 컨테이너(첫 번째와 마지막)를 만들어내며, 오직 그 두 컨테이너만 변형됩니다.
해시에서도 같은 개념이 적용됩니다.
my %h = a => 42, b => 666;
$_++ for %h.values; # 각 값을 증가
say %h; # {a => 43, b => 667}
사용자 코드에서 직접 바인딩하기
두 스칼라를 명시적으로 바인드할 수 있습니다.
my $a = 10;
my $b := $a; # $b를 $a의 컨테이너에 바인드
$b = 20; # $a도 20이 됨
say $a; # 20
Source: …
a = 42;
my $b := $a; # $b is bound to $a’s container
$b = 666; # stores into the same container
say $a; # 666
$b에 값을 할당하면 실제로 $a 안에 있는 컨테이너에 저장됩니다.
존재하지 않는 배열 요소에 할당하기
Raku에서는 아직 존재하지 않는 배열 인덱스에도 할당할 수 있습니다:
my @a;
@a[3] = 42;
say @a; # [(Any) (Any) (Any) 42]
초기에는 인덱스 3에 컨테이너가 없지만, 할당이 이루어지면 즉시 생성됩니다.
비밀은 다시 컨테이너 디스크립터에 있습니다.
다음과 같이 의사코드를 다듬어 보겠습니다:
# Pseudocode – not real Raku
class Scalar {
has $!descriptor; # holds name, type, defaults, etc.
has $!value;
method STORE($value) {
$!value := $!descriptor.process($value);
}
method FETCH() {
$!value;
}
}
이제 STORE는 $!descriptor.process($value)에 작업을 위임합니다. 이 메서드는 다음을 할 수 있습니다:
- 변수의 이름을 기억한다.
- 타입 제약을 강제한다.
- 기본값을 제공한다.
- 컨테이너가 필요로 하는 기타 bookkeeping 작업을 수행한다.
정리
- 컨테이너는 Raku 변수 모델의 핵심이다.
- 할당(
=)은 바인딩(:=)에 대한 설탕(sugar)일 뿐이다. is rw트레이트와return‑rw는 값이 아니라 컨테이너를 반환하도록 해준다.- 배열, 해시, 슬라이스, 그리고 루프 토픽까지 모두 컨테이너에 바인딩함으로써 동작한다.
- 컨테이너 디스크립터는 타입 검사, 기본값, 기타 메타데이터를 처리한다.
이 개념들을 이해하면 “마법처럼 보이는” Raku 기능들이 어떻게 동작하는지 바로 알 수 있고, 빠르고 관용적인 코드를 작성할 자신감도 생깁니다. 즐거운 컨테이너 바인딩 되세요!
Raku의 컨테이너와 프록시
Rakudo 구현에는 ContainerDescriptor:: 이름으로 시작하는 다양한 종류의 컨테이너 디스크립터 클래스가 존재합니다. 이들 모두는 구현 세부 사항으로 간주됩니다.
배열 및 해시 디스크립터
배열에서 존재하지 않는 요소에 AT‑POS 메서드가 호출될 때, 해당 요소가 어느 Array에 속하고 어떤 인덱스에 저장되어야 하는지를 알고 있는 특수 타입의 디스크립터를 가진 컨테이너가 생성됩니다.
AT‑KEY(파트 10에서 본 바와 같이)로 반환된 컨테이너의 디스크립터도 마찬가지이며, 할당될 때 어느 Hash에 저장해야 하는지와 어떤 키를 사용할지를 알고 있습니다.
이러한 특수 동작 덕분에 배열과 해시는 실제로 DWIM(뭘 하든 알아서) 할 수 있습니다.
이 디스크립터들은 원거리 행동 기능을 도입하는데, 이는 여러분이 좋아할 수도, 그렇지 않을 수도 있습니다:
my @a;
my $b := @a[3];
say @a; # []
$b = 42;
say @a; # [(Any) (Any) (Any) 42]
Note: 바인딩 후에는 배열의 요소가 초기화되지 않았으며,
$b에 값이 할당될 때 비로소 초기화됩니다. 이 동작은 실수로 인한 자동 활성화를 방지하기 위해 의도적으로 구현되었습니다.
실수로 인한 자동 활성화 방지
my %h;
say %h:exists; # False
say %h; # {}
%h = 42;
say %h; # {a => {b => 42}}
%h가 Associative로 간주되더라도 :exists 테스트는 %h 안에 Hash를 생성하지 않습니다. 값이 할당된 이후에야 %h와 그 중첩 구조가 실제로 살아납니다.
Source: …
Proxy 클래스
Raku는 완전히 사용자 정의 가능한 클래스를 제공하는데, 이를 통해 자신만의 컨테이너 로직을 만들 수 있습니다: Proxy. 컨테이너가 어떻게 동작하는지 이해하면 이 클래스를 사용할 때 도움이 됩니다.
간단한 Proxy 만들기
필요한 것은 다음 두 가지뿐입니다:
- 값을 가져오는 메서드 (
FETCH) - 값을 저장하는 메서드 (
STORE)
이 메서드들은 Proxy.new에 명명된 인수로 전달됩니다. 일반적인 패턴은 편의를 위해 생성 과정을 서브루틴으로 감싸는 것입니다:
sub answer is rw {
my $value = 42;
Proxy.new(
FETCH => method () {
$value
},
STORE => method ($new) {
say "storing $new";
$value = $new;
}
)
}
my $a := answer; # 바인드, 할당이 아님
say $a; # 42
$a = 666; # storing 666
say $a; # 666
- 서브루틴에
is rw트레이트가 필요합니다; 그렇지 않으면Proxy가 반환될 때 컨테이너가 해제됩니다. answer를 호출한 결과는 할당(=)이 아니라 바인드(:=)해야 합니다; 할당하면 역시 컨테이너가 해제되어 목적에 어긋납니다.
제공된 메서드들이 렉시컬 변수 $value를 클로저로 잡고 있기 때문에, 그 변수는 Proxy 객체가 파괴될 때까지 살아 있어 프록시가 값을 쉽게 저장할 수 있게 합니다.
FETCH/STORE 내부의 유연성
제공된 메서드 안에서는 원하는 어떤 코드든 자유롭게 실행할 수 있습니다. 예를 들어, Hash::MutableKeys 배포판은 이 기능을 활용합니다(또 다른 BDD – Blog Driven Development 사례).
Proxy 객체에 대한 디스크립터가 필요하다면 직접 만들 수 있습니다; 기본 Proxy는 최대한의 유연성을 제공하기 위해 디스크립터를 포함하지 않습니다.
에피소드 요약
This is the twelfth episode of “Cases of UPPER Language Elements in the Raku Programming Language,” the fifth episode discussing interface methods.
- Containers는 배열과 해시를 위한 특수 디스크립터를 포함하여 설명되었습니다.
- The
Proxyclass가 소개되었으며,FETCH와STORE라는 명명된 인자를 사용해 완전히 사용자 정의된 컨테이너를 만드는 방법을 보여주었습니다.
Stay tuned for the next episode!