Phasers
Phasers
어떤 블록이 시작할 때와 나올 때, 또는 루프가 한 바퀴 돌 때 같은 특정 시점에 자동으로 실행되는 코드가 있으면 편할 때가 많아요. 설정과 정리를 같은 위치에 쓰고 싶다거나, 루프 매 반복마다 초기화를 하고 싶다거나요. Raku의 phaser는 그런 '실행 시점'에 걸려서 자동으로 불리는 블록이에요.
본문
프로그램의 수명(실행 타임라인)은 여러 단계(phase)로 나뉘어요. phaser는 특정 실행 단계에서 호출되는 코드 블록이에요.
Phasers
phaser 블록은 그걸 포함하는 클로저의 트레잇(trait)일 뿐이며, 적절한 순간에 자동으로 호출돼요. 이렇게 자동 호출되는 블록을 phaser라고 불러요. 일반적으로 계산의 한 단계에서 다른 단계로 전환되는 지점을 표시하기 때문이에요. 예를 들어 CHECK 블록은 컴파일 유닛의 컴파일이 끝날 때 호출돼요. 다른 종류의 phaser도 설치할 수 있는데, 이들은 상황에 맞는 여러 시점에 자동으로 호출되며, 일부는 다양한 제어 예외(control exception)와 종료 값에 반응해요. 예를 들어 어떤 phaser는 블록을 나올 때 성공했는지 여부에 따라 호출될 수 있어요. 이때 성공은 정의된 값이나 목록을 반환하면서 진행 중에 어떤
요약은 다음과 같아요:
BEGIN {...} # * 컴파일 시간, 가능한 빨리, 단 한 번만 실행
CHECK {...} # * 컴파일 시간, 가능한 늦게, 단 한 번만 실행
INIT {...} # * 런타임, 가능한 빨리, 단 한 번만 실행
END {...} # 런타임, 가능한 늦게, 단 한 번만 실행
DOC [BEGIN|CHECK|INIT] {...} # 문서화 모드에서만
ENTER {...} # * 모든 블록 진입 시, 루프 블록에서는 반복
LEAVE {...} # 모든 블록 퇴출 시 (예외로 인한 스택 언윈드 포함)
KEEP {...} # 모든 성공적인 블록 퇴출 시, LEAVE 큐의 일부
UNDO {...} # 모든 실패한 블록 퇴출 시, LEAVE 큐의 일부
FIRST {...} # 루프 초기화 시, 어떤 ENTER보다 먼저
NEXT {...} # 루프 계속 시, 어떤 LEAVE보다 먼저
LAST {...} # 루프 종료 시, 어떤 LEAVE보다 나중에
PRE {...} # 모든 블록 진입 시 전제조건 어서트, ENTER보다 먼저
POST {...} # 모든 블록 퇴출 시 후조건 어서트, LEAVE보다 나중에
CATCH {...} # 예외 포착, LEAVE보다 먼저
CONTROL {...} # 제어 예외 포착, LEAVE보다 먼저
LAST {...} # whenever 블록이 tap하는 supply가 끝났을 때, 가장 마지막에 실행
QUIT {...} # whenever 블록 내 비동기 예외 포착, 가장 마지막에 실행
CLOSE {...} # supply 블록에 나타나며, supply가 닫힐 때 호출
*로 표시된 phaser는 런타임 값을 가지며, 주변 식보다 먼저 평가되면 나중에 나머지 식이 평가될 때 사용하기 위해 결과를 저장해요:
my $compiletime = BEGIN { now };
our $random = ENTER { rand };
다른 문장 접두어들처럼, 이 값 생성 구성체는 블록 앞이나 문장 앞에 놓을 수 있어요:
my $compiletime = BEGIN now;
our $random = ENTER rand;
대부분의 phaser는 블록이나 함수 참조를 받아요. 문장 형태는 어휘 스코프 선언을 '가두는' 일 없이 주변 어휘 스코프에 노출할 때 특히 유용해요. 이들은 앞선 예와 같은 변수·스코프를 선언하지만, 표시된 시점에 문장을 전체로 실행해요:
BEGIN my $compiletime = now;
ENTER our $random = rand;
(참고: 컴파일 시간에 계산된 변수 값은 주변 클로저의 런타임 클로닝 아래에서 유지되지 않을 수 있어요.)
대부분의 값 비생성 phaser도 그렇게 쓸 수 있어요:
END say my $accumulator;
다만 다음은 주의하세요:
END say my $accumulator = 0;
이것은 "my" 선언이 실제로 실행되는 시점이 END 시점이므로, 그때 변수를 0으로 설정해요. 문장 형태는 인자가 없는 phaser만 쓸 수 있어요. 이는 CATCH와 CONTROL이 항상 블록을 요구한다는 뜻이에요. 이들은 $_를 현재 topic으로 설정하는 인자를 받아서 내부가 switch 문처럼 동작할 수 있게 하기 때문이에요. (bare 문장을 허용하면 $_의 임시 결합이 CATCH나 CONTROL의 끝을 넘어 새어 나가서, 예측할 수 없고 꽤 심각한 결과를 낳을 수 있어요. 예외 핸들러는 불확실성을 줄이는 역할이지 늘리는 게 아니에요.)
이들 중 일부 phaser는 변수에 설정할 수 있는 대응 트레잇도 있어요. will 뒤에 소문자 phaser 이름을 쓰면 돼요. 이 형태는 해당 변수를 클로저의 topic으로 전달하는 이점이 있어요:
our $h will enter { .rememberit() } will undo { .forgetit() };
블록 내에서 여러 번 나타날 수 있는 phaser만 이 변수별 형태에 적합해요. CATCH와 다른 것들(CLOSE나 QUIT 같은)은 제외돼요.
phaser 밖의 블록 topic은 OUTER::<$_>로 여전히 사용 가능해요. 반환값을 수정할 수 있는지는 해당 phaser의 정책일 수 있어요. 특히 POST phaser 안에서는 반환값을 수정하면 안 되지만, LEAVE phaser는 더 자유로울 수 있어요.
메서드의 어휘 스코프에 정의된 phaser는 일반 어휘 변수뿐 아니라 self에 대해서도 닫히는 클로저예요. (동등하게, 구현이 모든 그런 phaser를 프라임된 invocant가 현재 객체인 submethod로 바꿀 수도 있어요.)
여러 phaser가 같은 순간에 실행되도록 예약되었을 때, 일반적인 타이브레이킹 원칙은 초기화 phaser는 선언된 순서대로, 종료 phaser는 반대 순서로 실행된다는 거예요. 설정(setup)과 정리(teardown)가 보통 서로 반대 순서로 일어나는 걸 원하기 때문이에요.
실행 순서 (Execution order)
컴파일 시작:
BEGIN {...} # 컴파일 시간, 가능한 빨리, 단 한 번만 실행
CHECK {...} # 컴파일 시간, 가능한 늦게, 단 한 번만 실행
실행 시작:
INIT {...} # 런타임, 가능한 빨리, 단 한 번만 실행
블록 실행 시작 전:
PRE {...} # 모든 블록 진입 시 전제조건 어서트, ENTER보다 먼저
루프 실행 시작:
FIRST {...} # 루프 초기화 시, 어떤 ENTER보다 먼저
블록 실행 시작:
ENTER {...} # 모든 블록 진입 시, 루프 블록에서는 반복
예외가 일어날 수도 있음:
CATCH {...} # 예외 포착, LEAVE보다 먼저
CONTROL {...} # 제어 예외 포착, LEAVE보다 먼저
루프 끝 (계속 또는 완료):
NEXT {...} # 루프 계속 시, 어떤 LEAVE보다 먼저
LAST {...} # 루프 종료 시, 어떤 LEAVE보다 나중에
블록 끝:
LEAVE {...} # 블록 퇴출 시, 예외로 인한 스택 언윈드 포함
KEEP {...} # 모든 성공적인 블록 퇴출 시, LEAVE 큐의 일부
UNDO {...} # 모든 실패한 블록 퇴출 시, LEAVE 큐의 일부
블록 후조건:
POST {...} # 모든 블록 퇴출 시 후조건 어서트, LEAVE보다 나중에
비동기 whenever 블록 완료:
LAST {...} # done으로 정상 종료했다면 블록 후 한 번 실행
QUIT {...} # 비동기 예외 포착
프로그램 종료:
END {...} # 런타임, 가능한 늦게, 단 한 번만 실행
프로그램 실행 phaser
BEGIN
phaser 안의 코드가 컴파일되자마자, 컴파일 시간에 실행하며 단 한 번만 실행돼요. 반환값은 이후 단계에서 사용할 수 있어요:
say "About to print 3 things";
for ^3 {
say ^10 .pick ~ '-' ~ BEGIN { say "Generating BEGIN value"; ^10 .pick }
}
# OUTPUT:
# Generating BEGIN value
# About to print 3 things
# 3-3
# 4-3
# 6-3
phaser 안의 ^10 .pick은 한 번만 생성되고 이후 루프가 런타임에 재사용해요. BEGIN 블록의 say가 루프 위의 say보다 먼저 실행된다는 점에 주목하세요.
CHECK
컴파일 시간에 가능한 한 늦게 실행하며, 단 한 번만 실행돼요. 이후 단계에서도 제공되는 반환값을 가질 수 있어요.
런타임에 생성된 코드는 여전히 CHECK와 INIT phaser를 발화시킬 수 있어요. 물론 그 phaser들이 시간을 거슬러 여행이 필요한 일을 할 수는 없지만요. 그건 웜홀이 필요해요.
INIT
컴파일 후 주 실행 동안에 가능한 한 빨리 실행하며, 단 한 번만 실행돼요. 이후 단계에서도 제공되는 반환값을 가질 수 있어요.
phaser들이 서로 다른 모듈에 있을 때, INIT와 END phaser는 사용하는 모듈의 use 시점에 선언된 것처럼 취급돼요. (다만 모듈을 두 번 이상 사용하면 phaser는 첫 번째로 발견될 때만 설치되므로, 이 순서에 의존하는 것은 잘못된 일이에요.)
런타임에 생성된 코드는 여전히 CHECK와 INIT phaser를 발화시킬 수 있어요. 물론 그 phaser들이 시간을 거슬러 여행이 필요한 일을 할 수는 없지만요. 그건 웜홀이 필요해요.
INIT는 클론된 클로저의 모든 복사본에 대해 한 번만 실행돼요.
END
컴파일 후 주 실행 동안에 가능한 한 늦게 실행하며, 단 한 번만 실행돼요. 열려 있는 핸들을 자동으로 닫아요.
phaser들이 서로 다른 모듈에 있을 때, INIT와 END phaser는 사용하는 모듈의 use 시점에 선언된 것처럼 취급돼요. (다만 모듈을 두 번 이상 사용하면 phaser는 첫 번째로 발견될 때만 설치되므로, 이 순서에 의존하는 것은 잘못된 일이에요.)
블록 phaser
블록 컨텍스트의 실행에는 자신만의 단계가 있어요.
블록을 떠나는 phaser들은 실제로 호출 스택이 언윈드될 때까지 기다렸다가 실행돼요. 언윈드는 어떤 예외 핸들러가 그 방식으로 예외를 처리하기로 결정한 뒤에만 일어나요. 즉 스택 프레임을 지나 예외가 던져졌다고 해서 공식적으로 블록을 떠난 것은 아니에요. 예외가 재개 가능(resumable)할 수 있기 때문이에요. 어쨌든 예외 핸들러는 예외가 재개 가능한지와 무관하게 실패한 코드의 동적 스코프 안에서 실행되도록 지정돼요. 스택이 언윈드되고 phaser가 호출되는 건 예외가 재개되지 않을 때만이에요.
이들은 블록 내에서 여러 번 나타날 수 있어요. 그래서 정확히 말해 트레잇이 아니라, 실제 트레잇에 저장된 목록에 스스로를 추가해요. 블록의 ENTER 트레잇을 검사해 보면 실제로는 단일 phaser가 아니라 phaser들의 목록이라는 것을 알 수 있어요.
이 phaser 블록들은 모두 이전에 선언된 어휘 변수를 볼 수 있어요. 변수가 클로저가 호출될 때 아직 정교화(elaborate)되지 않았더라도 말이죠 (그 경우 변수는 정의되지 않은 값으로 평가돼요).
ENTER
모든 블록 진입 시 실행하며, 루프 블록에서는 반복돼요. 이후 단계에서도 제공되는 반환값을 가질 수 있어요. ENTER phaser에서 던져진 예외는 ENTER 큐를 중단시키지만, LEAVE phaser에서 던져진 예외는 그렇지 않아요.
LEAVE
모든 블록 퇴출 시(예외로 인한 스택 언윈드 포함) 실행돼요. 다만 프로그램이 갑작스럽게 종료될 때는(예: exit로) 제외해요.
주어진 블록의 LEAVE phaser는 어떤 CATCH·CONTROL phaser보다 반드시 나중에 평가돼요. 여기에는 LEAVE 변형인 KEEP와 UNDO도 포함돼요. POST phaser는 다른 모든 것보다 나중에 평가되어, 심지어 LEAVE phaser도 후조건을 위반할 수 없게 보장해요.
ENTER phaser에서 던져진 예외는 ENTER 큐를 중단시키지만, LEAVE phaser에서 던져진 예외는 그렇지 않아요.
POST가 실패하거나 어떤 종류의 LEAVE 블록이 스택 언윈드 중에 예외를 던지면, 언윈드는 계속되고 처리할 예외들을 수집해요. 언윈드가 완료되면 그 시점부터 모든 새 예외가 던져져요.
sub answer() {
LEAVE say „I say after the return value.“;
42 # 이것이 반환값
}
참고: 루틴의 블록 안에 직접 있는 LEAVE phaser에 주의하세요. 잘못된 인자로 루틴을 호출하려는 시도 때도 실행될 거예요:
sub foo (Int) {
say "Hello!";
LEAVE say "oh noes!"
}
try foo rand; # OUTPUT: «oh noes!»
서브루틴 본문은 실행되지 않았지만, sub가 LEAVE phaser가 실행됐어요.
KEEP
모든 성공적인 블록 퇴출 시 LEAVE 큐의 일부로 실행돼요 (동일한 실행 순서를 공유해요).
UNDO
모든 실패한 블록 퇴출 시 LEAVE 큐의 일부로 실행돼요 (동일한 실행 순서를 공유해요).
PRE
모든 블록 진입 시 전제조건을 어서트해요. ENTER 단계보다 먼저 실행돼요. PRE phaser는 어떤 ENTER나 FIRST보다 먼저 발화돼요. 실패한 PRE·POST phaser가 던지는 예외는 같은 블록의 CATCH로는 잡을 수 없어요. 이는 PRE phaser가 실패하면 POST phaser가 실행되지 않는다는 뜻이에요.
POST
모든 블록 퇴출 시 후조건을 어서트해요. LEAVE 단계보다 나중에 실행돼요. KEEP·POST처럼 스코프를 정상적으로 나올 때 실행되는 phaser의 경우, 그 스코프의 반환값(있다면)이 phaser 안에서 현재 topic으로 사용 가능해요.
POST 블록은 두 가지 방식 중 하나로 정의할 수 있어요. 해당 POST가 별도의 phaser로 정의되면, PRE와 POST는 어휘 스코프를 공유하지 않아요. 반대로 어떤 PRE phaser는 대응 POST를 그 PRE의 어휘 스코프에 닫히는 임베디드 phaser 블록으로 정의할 수 있어요.
POST가 실패하거나 어떤 종류의 LEAVE 블록이 스택 언윈드 중에 예외를 던지면, 언윈드는 계속되고 처리할 예외들을 수집해요. 언윈드가 완료되면 그 시점부터 모든 새 예외가 던져져요.
실패한 PRE·POST phaser가 던지는 예외는 같은 블록의 CATCH로는 잡을 수 없어요. 이는 PRE phaser가 실패하면 POST phaser가 실행되지 않는다는 뜻이에요.
루프 phaser
FIRST, NEXT, LAST는 루프의 어휘 스코프 안에서만 의미가 있으며, 그런 루프 블록의 최상위에서만 나타날 수 있어요.
FIRST
루프 초기화 시 ENTER보다 먼저 실행돼요.
NEXT
루프가 계속될 때(next로 인해서든, 루프 바닥에 도달해서 다시 돌아가기 때문이든) LEAVE보다 먼저 실행돼요.
NEXT는 루프 블록의 끝이 정상적으로 도달되거나, 명시적 next가 실행될 때만 실행돼요. LEAVE phaser와 달리, NEXT phaser는 next가 던지는 제어 예외가 아닌 다른 예외로 루프 블록을 나가면 실행되지 않아요. 특히 last는 NEXT phaser의 평가를 건너뛰어요.
LAST
루프가 조건이 충족돼서 끝났거나 last로 나올 때 실행되며, LEAVE보다 나중에 실행돼요.
예외 처리 phaser
CATCH
현재 블록이 예외를 일으켰을 때 LEAVE 단계보다 먼저 실행돼요. <예외 포착>도 참고하세요.
CONTROL
현재 블록이 제어 예외를 일으켰을 때 LEAVE 단계보다 먼저 실행돼요. return, fail, redo, next, last, done, emit, take, warn, proceed, succeed가 일으켜요.
say elems gather {
CONTROL {
when CX::Warn { say "WARNING!!! $_"; .resume }
when CX::Take { say "Don't take my stuff"; .resume }
when CX::Done { say "Done"; .resume }
}
warn 'people take stuff here';
take 'keys';
done;
}
# OUTPUT:
# WARNING!!! people take stuff here
# Don't take my stuff
# Done
# 0
비동기 phaser
LAST (비동기)
done 호출로 끝나거나 supply 블록이 정상 종료될 때 실행돼요. 자신이 놓여 있는 whenever 블록이 끝난 다음에 완전히 실행돼요.
이 phaser는 LAST라는 이름을 재사용하지만 루프 LAST phaser와는 다르게 동작해요. tap으로 supply를 탭하면서 done 루틴을 설정하는 것과 비슷해요.
QUIT
whenever 블록이 끝난 다음에 실행돼요.
이 phaser는 tap으로 Supply를 탭하면서 quit 루틴을 설정하는 것과 비슷해요.
CLOSE
supply 또는 react 블록에 나타나요. supply가 닫히거나 react가 모든 whenever를 소진했을 때 호출돼요.
DOC phaser
DOC 키워드를 붙이면 BEGIN, CHECK, INIT phaser가 문서화 모드에서만 실행돼요. 컴파일러는 --doc로 실행될 때 문서화 모드에 있어요.
DOC INIT { say 'init' } # 문서화 모드에서 초기화 시 'init'을 출력