트랙
/
Factor
Factor
/
연습 문제
/
신호 스텐실
신호 스텐실

신호 스텐실

학습 연습 문제

소개

이미 조각들을 모아 쿼테이션을 만들고 실행하는 방법을 알고 있어요. 쿼테이션은 그저 코드의 나열일 뿐이라서, 이미 알고 있는 시퀀스 단어들을 그대로 적용할 수 있어요:

USING: kernel math prettyprint quotations sequences ;

{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call .   ! => 9
[ 1 + ] dup append .                                 ! => [ 1 + 1 + ]

append는 두 쿼테이션을 이어 붙이고, concat은 쿼테이션 배열을 평평하게 펼치고, map은 데이터를 쿼테이션으로 바꾸고, >quotation은 그 결과를 호출할 수 있게 만들어요. MACRO:는 바로 이 기술을 가져다 컴파일 타임으로 옮겨요.

매크로란 무엇인가요

MACRO: 정의는 평범한 단어처럼 보이지만, 그 본문은 프로그램이 실행될 때가 아니라 컴파일되는 동안 실행돼요. 본문은 스택에 쿼테이션 하나를 남겨야 해요. 그러면 컴파일러가 그 쿼테이션을 호출 지점에 끼워 넣어요. 매크로 호출을 본문이 만들어 낸 코드로 바꾸는 거예요. 실행 시점에는 매크로가 남아 있지 않고, 생성된 코드만 있어요.

USING: kernel macros sequences ;

MACRO: twice ( quot -- spliced )
    dup append ;

[ 1 + ] twice를 어디에 쓰든, 컴파일러는:

  1. 스택에 [ 1 + ]를 올려 둔 채로 본문을 실행하고,
  2. dup append가 [ 1 + 1 + ] 쿼테이션을 만들고,
  3. 그 코드 1 + 1 +를 그 자리에 끼워 넣어요.

그래서 twice를 호출하는 단어는 손으로 1 + 1 +를 쓴 것처럼 컴파일돼요:

: inc-twice ( x -- y ) [ 1 + ] twice ;

5 inc-twice .   ! => 7

핵심적인 차이는 이래요. 쿼테이션을 받는 일반적인 단어는 실행 시점에 그것을 call해요. 매크로는 아무것도 호출하지 않아요. 대신 컴파일러에게 새로운 코드를 건네서 그 자리에 대신 컴파일하게 해요.

입력은 리터럴이어야 해요

본문이 컴파일 타임에 실행되기 때문에, 매크로의 입력은 그 시점에 알려져 있어야 해요. 호출 지점에서 리터럴이어야 하는 거죠. [ … ] 쿼테이션, 숫자, 배열, 문자열 같은 것들이요. 실행 시점에만 존재하는 값은 매크로에 건넬 수 없어요.

! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice          ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot  ! ok: the quotation and the number are literals

도구 모음

매크로 본문은 쿼테이션을 반환하는 평범한 코드예요. 이미 만난 단어들이 대부분의 일을 해내요:

만들고 싶은 것… 사용할 단어
[ A ]와 [ B ]로 [ A B ] append
쿼테이션 배열로 쿼테이션 하나 concat >quotation
시퀀스의 각 원소마다 쿼테이션 map, 그다음 concat >quotation

sequences와 kernel에 있는 단어 두 개가 도구 모음을 완성해요.

**<repetition>**은 sequences에 있는데, 한 원소를 n개 복사한 시퀀스를 만들어요. 스택 효과는 ( length elt -- repetition )이고, 개수가 먼저 와요. 결과는 가상 시퀀스라서, 평소처럼 펼쳐 주면 돼요:

3 [ 1 + ] <repetition> concat >quotation .   ! => [ 1 + 1 + 1 + ]

**curry**는 kernel에 있는데, 쿼테이션 앞에 값을 붙여서 그 값을 먼저 스택에 올리는 새 쿼테이션을 만들어요:

3 [ + ] curry .          ! => [ 3 + ]
{ 1 2 3 } [ . ] curry .  ! => [ { 1 2 3 } . ]

curry는 특정 원소 하나를 쿼테이션에 구워 넣는 방법이에요. { 10 20 30 }과 [ + ]를 정확히 [ 10 + ] [ 20 + ] [ 30 + ]로 바꿔 주는 거죠.

생각보다 간단한 이유

어떤 언어에서는 매크로에 특별한 quote/unquote 문법과 이름 충돌을 피하기 위한 별도의 장치가 필요해요. Factor에는 그런 게 전혀 필요 없어요. 쿼테이션은 이미 해석이 끝난 단어들의 나열이거든요. 파서가 [ … ]를 읽을 때 각각의 dup, +, append를 찾아 두니까요. 그래서 매크로 본문은 이미 하고 있는 시퀀스 조작일 뿐이고, 그게 우연히 쿼테이션을 반환할 뿐이에요.

가까운 사촌: $[ … ]

매크로는 호출에 끼워 넣을 쿼테이션을 반환해요. 그 대신 파싱 시점에 계산되는 평범한 값을 원한다면, $[ code ]가 파일을 읽는 동안 code를 실행하고 그 결과를 리터럴로 소스에 접어 넣어요:

! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;

CONSTANT: build-time $[ now ]

$[ … ]는 literals 보캐뷸러리에 있는데, 파싱 시점의 결과를 리터럴에 끼워 넣는 단어들을 모아 둔 곳이에요. 완전성을 위해 언급했어요. 여러분이 작성하게 될 네 가지 스텐실, twice, repeat-quot, compose-many, each-literal은 모두 MACRO: 선언이에요.

지침

신호소의 코드 생성 툴킷을 관리하고 있어요. 각 매크로는 스텐실, 즉 컴파일러가 호출 지점마다 특수화된 코드를 찍어 내는 데 쓰는 작은 템플릿이에요.

직접 작성할 것은 네 개의 매크로뿐이에요. 테스트 파일은 여러분의 매크로를 호출하는 작은 래퍼 워드들을 정의하고(매크로는 자신이 생성하는 코드를 통해서만 볼 수 있어요), 이어서 래퍼들의 런타임 결과를 검사해요. 래퍼는 그대로 두면 돼요.

각 과제에서 매크로 본문은 호출 지점의 리터럴 입력을 스택으로 받고, 쿼테이션 하나를 남겨야 해요. '컴파일 결과' 줄은 쿼테이션이 만들어 내야 하는 직선 코드를 보여줘요.

1. 두 번 실행

twice는 리터럴 쿼테이션을 받아서, 원본을 두 번 연달아 실행하는 쿼테이션을 반환해요.

! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ]  ->  must build [ 1 + 1 + ]
5 inc-twice .   ! => 7

2. N번 반복

repeat-quot은 리터럴 쿼테이션과 리터럴 개수 n을 받아서, 원본을 n번 연달아 실행하는 쿼테이션을 반환해요. n = 0이면 빈 쿼테이션 [ ]를 반환해요.

! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3  ->  must build [ 1 + 1 + 1 + ]
5 inc-thrice .   ! => 8

3. 여러 개 합성하기

compose-many는 쿼테이션들이 담긴 리터럴 배열을 받아서, 각 쿼테이션을 차례로 실행하는 쿼테이션 하나를 반환해요. 빈 배열이면 빈 쿼테이션을 반환해요.

! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] }  ->  must build [ 1 + 2 * 1 - ]
5 pipeline .   ! => 11

4. 전개된 반복

each-literal은 리터럴 시퀀스와 쿼테이션을 받아요. 시퀀스의 각 원소를 스택에 올린 다음 쿼테이션을 실행하는 쿼테이션을 반환하죠. 컴파일 시간에 일어나는 반복이라, 생성되는 코드는 직선 코드예요.

! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ]  ->  must build [ 10 + 20 + 30 + ]
sum-three .   ! => 60

이 넷 중에서는 생성되는 코드의 구조가 가장 크게 달라지는 과제예요. 시퀀스의 길이와 원소 값이 모두 코드의 모양을 결정하죠. 힌트에서 한 단계씩 만들어 가는 과정을 안내해요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Factor Exercism

신호 스텐실 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Factor 트랙을 개념 47개연습 문제 163개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.