Par 언어 테스트 프레임워크 구현 -- Iterative Box Choice 패턴 적용
notJoon @joonnot@hackers.pub
⚠️ 아직 PR이 머지가 된게 아니기 때문에 이 문서의 내용은 바뀔 수 있습니다. 실제 코드는 https://github.com/faiface/par-lang/pull/66 를 탐고해주세요.
Par 언어에 테스트 프레임워크를 구현하며 난감한 구현 문제에 직면했었습니다. 하나의 테스트 함수에서 여러 assertion을 처리할 수 있는 Test
타입을 만들고자 했지만, 기존 타입 시스템의 consumption 동작이 걸림돌이 되었습니다.
타입 소비
Par의 타입 시스템에서 box choice
타입은 값을 사용하면 한 번 사용 후 소비되어 버리는 특성을 가지고 있습니다. 이 동작은 실수로 인한 리소스의 재사용을 방지하는 역할을 하기 때문에 상당히 유용한 동작이지만, 여러 assertion을 순차적으로 처리해야 하는 테스트에서는 난감한 상황이 발생합니다.
예를 둘어, 다음과 같은 상황을 생각해볼 수 있습니다.
// 가상의 일반 box choice 타입
type Test = box choice {
.assert(String, Bool) => !
}
def TestBlah = [test: Test] do {
test.assert("첫 번째 테스트", .true!) // OK - test가 여기서 소비됨
test.assert("두 번째 테스트", .false!) // 에러! 'test'가 더 이상 정의되지 않음
} in !
box choice
타입을 사용해 구현한 Test
타입을 구현하면 TestBlah
의 동작은 첫 번째 테스트만 실행하고 바로 터져버립니다. 왜냐하면 이미 첫번째 assertion에서 test
를 소비해버렸기 때문에 두번째 assertion에서 활용할 수 있는 test
에 대한 정보가 존재하지 않습니다.
사실 Test
타입을 처음 제작했을 때는 이걸 미쳐 생각하지 못했다가 프로포절 리뷰 과정에서 얘기가 나와 수정을 하게 되었습니다.
I'm saying iterative box choice instead of box choice because a plain box choice would get consumed in process syntax:
~~~
def TestBlah = [test: Test] do {
test.assert("Blah", .true!)
test.assert("Blah", .false!) // `test` not defined anymore
} in !
~~~
But the above type works well for well for both process style, and expressions
Check out handle.provide_box to construct box from externals
And just to note, .complete => ! is not necessary anymore for a box choice (or iterative box choice) because it can be dropped implicitly
[실제 대화 내역. 이미지 첨부를 어떻게 할지 몰라서 대화를 그대로 복사했습니다]
Iterative Box Choice
다행히 Par 언어의 타입 정의 방식은 각각의 unit 타입들을 조합해서 정의하는 방식으로 구현이 되어 있습니다.[1] 이미 iterative
도 box
(실제로는 box_
)도, choice
타입이 구현되어 있었기 때문에 제가 할 일은 그냥 이 타입들을 하나로 묶으면 되는 것이였습니다.
pub fn iterative_box_choice(
label: Option<&'static str>,
branches: Vec<(&'static str, Self)>,
) -> Self {
Self::iterative(label, Self::box_(Self::choice(branches)))
}
각 타입은 다음과 같은 기능을 제공합니다.
iterative
: 반복 작업 가능box
: 동적 동작을 위한 힙 할당 제공choice
: 여러 메서드 중 선택 가능
그리고 iterable box choice 타입은 단어 그대로의 동작을 합니다.
- 여러 번 사용 가능
- 메서드 선택 제공
- 외부 구현과 연동 가능
아무튼, 이 타입 덕분에 기존 box choice
에서는 불가능했던 여러 assertion들을 처리할 수 있게 되었습니다. 약간 이런 식으로요:
일반 box choice (한 번만 사용 가능)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎫 → [assert] → ✅ → (티켓 소진 ❌)
iterative box choice (반복 사용 가능)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎫 → [assert] → ✅ → 🎫 (새 티켓 발급)
↓
[assert] → ✅ → 🎫
↓
[assert] → ✅ → 🎫
↓
[done] → 🏁
[claude가 그렸습니다.]
타입 구조는 다음과 같습니다.
iterative { // 반복 가능한 컨테이너
box { // 동적 할당 (외부 구현 연결)
choice { // 메서드 선택지
.assert => ... // 테스트 수행
.done => ... // 종료
}
}
}
이제 각 반복이 새로운 핸들을 제공하기 때문에 테스트 실행 함수도 수정을 해야했습니다. 새로운 구현은 재귀를 사용해서 처리를 하도록 했습니다.
fn provide_test_inner(handle: Handle, sender: mpsc::Sender<AssertionResult>) {
handle.provide_box(move |mut handle| {
async move {
match handle.case().await.as_str() {
"assert" => {
// assertion 처리...
let _ = sender.send(result);
// 다음 반복을 위한 재귀 호출
provide_test_inner(handle, sender);
}
"done" => {
handle.break_(); // 반복 종료
}
}
}
});
}
결론
이 변경으로 이제 한번에 하나만 실행가능했던 테스트가 메서드 체이닝 방식, 순차적으로 처리하는 방식 두 가지 스타일이 가능하게 변경이 되었습니다.
Expression 스타일 (메서드 체이닝)
def TestMath: [Test] ! = [test] do {
test
.assert("2 + 3 = 5", Nat.Equals(Add(2, 3), 5))
.assert("3 * 4 = 12", Nat.Equals(Mul(3, 4), 12))
.assert("10 - 5 = 5", Nat.Equals(Sub(10, 5), 5))
} in !
Process 스타일 (순차적 명령문)
def TestWithCalculations: [Test] ! = [test] do {
test.assert("기본 덧셈", Nat.Equals(Add(1, 1), 2))
let result = Add(10, 20)
test.assert("계산된 합", Nat.Equals(result, 30))
let doubled = Mul(result, 2)
test.assert("30의 두 배", Nat.Equals(doubled, 60))
} in !