Forth
." Hello, world!"
1. 개요
포스 표준 홈페이지
찰스 무어(Charles H. Moore)에 의해 개발된 명령형, 절차적 스택 기반 프로그래밍 언어이다. 반영(Reflective), 연결(Concatenative) 프로그래밍 패러다임에도 속한다. 주로 임베디드 장치를 위해서 쓰여지지만 PC를 타겟으로 한 프로그램도 작성된다.
포스는 스택 기반으로 입력된 데이터가 스택에 쌓이며 이를 처리한다. 때문에 코드 상으로 보면 LISP와는 정 반대인 역폴란드 표기법으로 수식을 작성하게 된다.
1.1. 명칭
Forth라는 이름은 4세대(차세대) 프로그래밍을 뜻하는 FOURTH로 지어졌으나, 당시 사용하던 시스템이 파일 이름을 5자로 제한했기 때문에, FORTH로 쓰게 됐다. 옛날에는 포스의 이름을 전부 대문자로 적었기 때문에 현재도 FORTH로 쓰는 경우가 많다.
2. 역사
포스는 1970년대 이전부터 찰스 무어에 의해 개발되기 시작하였다.
널리 보급된 이후, 1979년과 1983년에 FORTH-79. FORTH-83으로 정리되었고, 이는 1994년 ANSI에 의해 표준화되었다.
포스는 가볍고 단순했기 때문에 1980년대 상당한 인기를 끌었다.
1987년 KAIST의 변종홍은 '늘품'이라는 한국어 포스를 개발했다. 포스의 확장성으로 단어를 한국어로 바꿔 만든 것이다. 이는 당시 마이크로소프트웨어, 과학동아 등지에도 소개됐다.
이후 포스의 스택 기반 프로그래밍이라는 개념은 Factor, Joy 언어, 그리고 포스트스크립트에도 영향을 주었다.
3. 문법
주로 쓰이는 구현체인 GForth의 매뉴얼에서 영어로 된 Forth 튜토리얼이 있어서 포스에 대해서 공부할 수 있다. 자세한 설명은 이곳에서 배우는 것이 좋다. GForth에서만 사용 가능한 것들도 포함되어있으나, 표준을 같이 병기해두므로 큰 문제는 없다.
3.1. 사칙연산
포스는 역폴란드 표기법으로 식을 기술한다. 단어들은 공백으로 구분된다.
'2 + 3 × 5'의 계산 결과를 출력하려면 다음과 같이 쓴다. 중위 표기법으로 적은 식에는 사칙연산의 우선 순위가 존재하기 때문에, 곱셈을 먼저 계산해주어야 한다.
2 3 5 * + .
- 스택에 값을 삽입해 (2, 3, 5)의 값이 있다.
- 연산자 * 에 의해 스택에는 (2, 15)의 값이 있다.
- 연산자 + 에 의해 스택에는 (17)의 값이 있다.
- 명령어 . 에 의해 스택의 가장 위에 있는 값 17이 출력된다. 스택에는 아무 값도 없다.
2 3 + 5 * .
- 스택에 값을 삽입해 (2, 3)의 값이 있다.
- 연산자 + 에 의해 스택에는 (5)의 값이 있다.
- 스택에 값을 삽입해 (5, 5)의 값이 있다.
- 연산자 * 에 의해 스택에는 (25)의 값이 있다.
- 명령어 . 에 의해 스택의 가장 위에 있는 값 25가 출력된다. 스택에는 아무 값도 없다.
3.2. 단어
포스는 단어를 정의하여 그 단어가 무슨 행동을 할 지를 정한다. 이는 다른 언어에서의 함수처럼 쓰인다. 차이점은 다른 언어에서의 함수는 입력되는 매개변수와 출력값이 존재하지만, 포스는 스택 기반 언어이기에 코드에 기술한 만큼의 스택을 소비하고 값을 스택에 남긴다. 포스는 이러한 스택의 변화를 단어를 정의할 때 소괄호를 활용하여 주석으로 적어 놓는다.
포스의 장점은 확장성으로 손꼽히는데, 그 이유는 단어를 정의함에 있어서 큰 제한이 없기 때문이다. 기존의 단어를 다시 재정의할 수도 있기 때문이다. 예컨대 연산자 + 를 다른 역할을 하게 만들거나, '더하기'란 이름의 연산자를 새로 만들어 더하기의 역할을 하게 만들 수 있다. 다만 포스는 단어의 대소문자를 구별하지 않는 특징이 있고, 포스 자체가 앞서 설명했듯 공백으로 단어를 구분하기 때문에 단어 중간에는 공백이 들어갈 수 없다.
: addprint ( n1 n2 -- n1+n2 )
+ dup .
;
위의 코드에서는 두 정수를 더하고 출력한 뒤, 다시 그 값을 스택에 남기는 단어인 addprint를 정의한다. : (콜론)은 단어를 정의하는 단어이다. 세미콜론 뒤에는 단어의 이름을 기술하고, ; (세미콜론)이 오기 전까지 단어의 행동을 기술한다.- : 를 통해 단어를 정의하기 시작한다.
- 단어의 이름은 addprint이다.
- 소괄호를 통해 스택의 변화를 주석으로 기술해놓았다.
- \-\- 앞은 소비할 값, 뒤는 내놓을 값이다.
- 두 정수 n1과 n2를 소비할 것임을 알려준다.
- n1과 n2를 더한 값을 내놓을 것임을 알려준다.
- 단어는 '+ dup .' 의 행동을 취한다.
- + 연산자를 통해 우선 스택의 두 값을 뽑아 더한다.
- dup 명령어를 통해 스택의 맨 위에 있는 값, 즉 앞에서 더한 값을 복사해 스택의 맨 위에 추가한다.
- . 명령어를 통해 스택의 맨 위에 있는 값을 출력한다. 스택에는 더한 값이 여전히 남았다.
- ; 를 통해 단어의 정의를 마친다.
3.2.1. 지역변수
단어에서 사용할 값이 여러 번의 소비를 거치는 경우에, dup 등의 스택 조작 명령어를 이용해 스택을 이용하는 것은 매우 번거로운 일이다. 따라서 단어에서의 지역 변수를 정의할 수 있다. 이는 다른 언어에서의 함수의 매개변수와 비슷한 사용법이다.
이를 이용해 앞서 정의한 addprint를 개조해본다.
: addprint2 { a b } ( n1 n2 -- n1+n2 )
a .
'+' emit space
b .
." = "
a b + dup .
;
- 중괄호 내에 들어가서 공백으로 구분된 단어들은 지역변수가 될 것이다.
여기서 a가 먼저 들어간 값, b가 나중에 들어간 값, 즉 스택의 맨 위에 있는 값이다.
- a 로 지역변수 a의 값을 스택에 넣는다. 이후 . 명령어로 출력했다. 이후 b도 마찬가지로 출력된다.
- '+' 는 문자 리터럴로 해당하는 문자의 아스키 코드값을 스택에 넣었다.
이후 emit 명령어로 그 코드에 맞는 문자를 출력한다.
- space 명령어로 공백 하나를 출력했다.
- ." 명령어는 공백 이후 " (쌍따옴표)까지 올 문자열을 출력한다.
따라서 "(a의 값) + (b의 값) = "의 문자열이 화면에 출력된다.
- a b 로 a와 b의 값을 스택에 넣고, 이전처럼 계산해서 출력한다.
스택에는 a와 b를 더한 값이 남는다.
예시로 적은 코드에 개행이 많이 되어있다. 개행엔 특별한 의미가 있는 것이 아니고 다른 공백과 하는 일이 같지만, 코드를 보기 쉽게 해 주는 장점이 있다. 실제로 코드를 한 줄로 이어 쓸 수도 있다. 간단한 단어의 경우에는 그렇게 정의하는 것이 선호된다.3.3. 제어문
포스에서는 제어문을 단어 정의 내에서만 사용할 수 있다. 제어문의 조건으로는 정수형 값이 쓰이는데, 0은 거짓, 0이 아닌 값은 참으로 취급된다. true 와 false 단어를 통해 참값을 표현할 수도 있다.
3.3.1. 조건문
3.3.1.1. if 문
if, else, then 단어로 조건 실행을 할 수 있다. gforth에선 then 대신 endif를 사용할 수 있다. 두 숫자 중 큰 숫자만을 남기는 단어 max를 만들어본다.
: max ( n1 n2 -- n )
2dup
< if
drop
else
nip
endif
;
- 2dup 명령어가 스택의 두 원소를 그대로 복제한다.
- < 를 통해 스택의 윗 값이 더 크면 true, 그렇지 않으면 false가 스택에 남는다.
- if 는 스택에서 값을 뽑아 참일 때와 거짓일 때를 가려 조건 실행을 한다.
if 이후 else나 endif가 오기 전까지의 코드는 참이면 실행된다.
참일 경우 drop 명령어를 통해 스택의 맨 윗 값을 버린다.
참일 경우 drop 명령어를 통해 스택의 맨 윗 값을 버린다.
- else 이후 endif가 오기 전까지의 코드는 거짓일 경우 실행된다.
이때 else는 꼭 없어도 된다. 거짓일 경우 nip 명령어를 통해 스택의 맨 위에서 바로 아랫 값을 버린다.
- endif로 조건문을 종료한다.
3.3.1.2. case 문
다른 언어에서의 switch ~ case 문과 유사하다.
case
1 of ." one" endof
2 of ." two" endof
3 of ." three" endof
2 2 + of ." four" endof
." other number"
endcase
위 코드는 case 문에서 스택에서 값을 뽑아 일치하는 값에 따라 코드를 실행한다.3.3.2. 반복문
3.3.2.1. begin ~ 문
begin으로 시작하는 반복문은 간단한 반복문이다. C언어나 다른 비슷한 언어에서의 무한루프, while문, do-whlie문에 해당한다. begin 단어는 일단 런타임에서 만나면 아무 일을 하지 않으며, 루프가 끝나면 점프해서 되돌아올 위치를 알려주는 역할을 한다.
0
begin
dup . 1 +
again
begin ~ again 문은 무한 루프이다. 이 코드는 0부터 숫자를 계속 증가시켜 나가면서 출력하는 코드이다. 'Ctrl + C'를 키보드로 눌러 인터럽트를 일으켜 탈출할 수 있다.0
begin
dup . 1 +
dup 10 >
until
이 코드는 0부터 10까지 출력한다. begin ~ until 문은 until문에서 조건을 판단하여 '''거짓'''이면 반복한다. 다른 언어에서의 do-while문과 비슷하다. 0
begin
1 +
dup 10 <
while
dup .
repeat
이 코드는 1부터 9까지 출력한다. 우선 begin문 이후의 코드를 실행한 뒤에 while문에서 조건을 판단하여 '''참'''이면 repeat까지 실행하고 반복한다. while에서 거짓일 경우 repeat 이후로 점프한다. 다른 언어에서의 while문과 비슷하다.3.3.2.2. 횟수 반복문
다른 언어에서의 for문과 유사하다. 횟수 반복문의 특징으로는 반복문을 세는 지역변수가 자동으로 생성된다는 점이다. 둘러싸인 순서대로 i, j, k의 세 가지 지역변수가 생성되어 사용할 수 있다.
3.3.2.2.1. ?do ~ loop 문
다음은 0부터 9까지 10개의 숫자를 출력하는 반복문이다.
10 0 ?do
i .
loop
GForth에서 숫자의 증감을 조절하려면 loop를 사용하는 대신 +loop 나 -loop 를 사용하면 된다. 이 둘은 앞에 오는 값을 뽑으면서 그 값만큼 증감하게 된다. +loop는 부호 있는 정수값, -loop는 부호 없는 정수값을 사용하며 '-1 +loop'와 '1 -loop'는 동일한 작동을 한다.다음은 10부터 1까지 10개의 숫자를 출력하는 반복문이다.
0 10 -do
i .
1 -loop
또한 GForth에서는 ?do 대신에 +do 나 -do 를 사용할 수 있다. 이는 안전한 방법으로써, 반복문의 시작과 끝을 잘못 적어 방향이 이상한 경우 반복을 허용하지 않는다. leave 를 사용하면 반복문을 도중에 빠져나오는 것이 가능하다. 다른 언어에서의 break 와 같다.
unloop exit 를 사용하는 것도 같은 효과를 가진다.
3.3.2.2.2. for ~ next 문
보다 간단한 반복문으로는 for ~ next 문을 사용할 수 있다. 다음은 이를 활용한 10부터 0까지 11개의 숫자를 출력하는 반복문이다.
10 for
i .
next
4. 구현체
4.1. GForth
홈페이지
GNU 프로젝트 페이지
GNU 프로젝트의 일부로서 관리되는 구현체이다. 프리웨어, 자유 소프트웨어이다. 1992년 중반 Bernd Paysan, Anton Ertl 그리고 Jens Wilke에 의해 개발되었다. GForth는 ANSI/200x Forth 표준을 준수하며, 다양한 프로세서의 리눅스, 마이크로소프트 윈도, 맥 OS, 안드로이드, 그리고 GForth EC 임베디드 시스템에서 구동된다.
GForth는 GCC로부터 직접적으로 빠르게, 혹은 간접적으로 스레드된 코드를 컴파일한다.
4.2. SwiftForth, SwiftX
홈페이지
Forth, Inc.로부터 개발되는, 네이티브 코드를 생성하는 상용 구현체이다.
SwiftForth는 32비트 x86 호환 CPU에서 리눅스, 마이크로소프트 윈도, 맥 OS에서 사용할 수 있으며 IDE가 제공, 사용할 수 있다. 디버깅 툴로 역 어셈블러, 역 컴파일러, 단계별 디버거가 포함된다.
SwiftX는 ARM을 비롯한 임베디드 시스템을 타겟으로 한 구현체이다.
4.3. pForth
홈페이지
Github
Phil Burk에 의해 C로 쓰여진 오픈 소스 구현체이며, 이식성이 특징이다. 리눅스, 마이크로소프트 윈도, 맥 OS와 많은 임베디드 시스템에서 사용할 수 있다.