GNU Prolog 대화형 인터프리터

GNU Prolog 대화형 인터프리터

GNU Prolog는 고전적인 Prolog 대화형 인터프리터(top-level)를 제공해요. 사용자는 이를 통해 질의를 실행하고, Prolog 프로그램을 조회(consult)하고, 나열하고, 실행하고, 디버깅할 수 있어요.

출처: GNU Prolog Manual - The GNU Prolog interactive interpreter

본문

GNU Prolog는 고전적인 Prolog 대화형 인터프리터를 제공해요. 이를 top-level이라고도 불러요. 사용자는 질의를 실행하고, Prolog 프로그램을 조회하고, 나열하고, 실행하고, 디버깅할 수 있어요.

top-level은 다음 명령으로 호출할 수 있어요:

% gprolog [ OPTION ]...     (% 기호는 운영체제 셸 프롬프트)

옵션:

  • --init-goal GOAL - top-level에 들어가기 전에 GOAL을 실행.
  • --consult-file FILE - top-level 안에서 FILE을 조회(consult).
  • --entry-goal GOAL - top-level 안에서 GOAL을 실행.
  • --query-goal GOAL - GOAL을 top-level에 대한 질의로 실행.
  • --help - 도움말 출력 후 종료.
  • --version - 버전 번호 출력 후 종료.
  • -- - 나머지 명령행을 파싱하지 않음.

gprolog 명령의 주 역할은 top-level 자체를 실행하는 것, 즉 내장 술어 top_level/0(8.18.1절)을 실행하는 것이며, 그러면 다음과 같이 출력돼요:

GNU Prolog 1.5.0 (64 bits)
Compiled May 3 2021, 16:36:43 with gcc
Copyright (C) 1999-2021 Daniel Diaz
| ?-

top-level은 다음 절에서 설명하는 대로 질의를 실행할 준비가 되어 있어요. top-level을 종료하려면 파일 끝(EOF) 키 시퀀스(Ctl-D)나 그 항 표현인 end_of_file을 입력하면 돼요. 내장 술어 halt/0(8.18.1절)을 사용할 수도 있어요.

top-level 자체에 들어가기 전에 명령행이 처리되어 위에 나열된 알려진 옵션을 모두 처리해요. 인식되지 않은 인자들은 모두 모아져서 인자 목록(argument list)을 구성하며, 이는 argument_value/2(8.27.2절)나 argument_list/1(8.27.3절)로 사용할 수 있어요. -- 옵션은 명령행 파싱을 멈추고, 남은 옵션들은 모두 인자 목록에 수집돼요.

사용자와의 상호작용에 들어가기 전에 목표(goal)를 실행하기 위한 여러 옵션이 제공돼요:

  • --init-goal 옵션은 GOAL을 만나자마자(명령행이 처리되는 동안) 실행해요. 따라서 GOAL은 top_level/0에 들어가기 전에 실행돼요.
  • --consult-file 옵션은 top_level/0에 들어갈 때 배너가 표시된 직후 FILE을 조회해요. (--consult-file 옵션들은 그 전의 --consult-file 옵션들보다 먼저 처리된다고 되어 있으나, 이는 문서의 원문 표현 그대로임)
  • --entry-goal 옵션은 top_level/0에 들어갈 때 배너가 표시된 직후 GOAL을 실행해요.
  • --query-goal 옵션은 사용자가 (top-level 아래에서) 타이핑한 것처럼 GOAL을 실행해요.

위 순서가 init, entry, query 각 종류의 목표가 실행되는 순서예요. 같은 종류의 목표가 여러 개 있으면 나타난 순서대로 실행돼요. 즉 모든 init 목표가(나타난 순서대로) 모든 entry 목표보다 먼저 실행되고, 모든 entry 목표는 모든 query 목표보다 먼저 실행돼요.

각 GOAL은 셸 인자(즉 하나의 셸 문자열)로 전달되며 마침표(.)로 끝나면 안 돼요. 예: sh 계열에서 --init-goal 'write(hello), nl'. GOAL을 실행하려면 read_term_from_atom(Goal, Term, [end_of_term(eof)])를 사용해 항(term)으로 변환해요.

셸 문자열 구문과 Prolog 구문을 모두 지키는 것은 까다로울 수 있어요. 예를 들어 백슬래시 문자 \를 전달하는 것은 sh와 Prolog 인용 원자(quoted atom) 양쪽에서 이스케이프 시퀀스를 도입하므로 어려울 수 있어요. 이때 백쿼트(back quote)를 쓰는 것이 유용할 수 있는데, 기본적으로 백쿼트 안에서는 이스케이프 시퀀스가 처리되지 않기 때문이에요(이 동작은 Prolog 플래그 back_quotes로 제어할 수 있어요, 8.22.1절).

Prolog 인자 목록은 전체 명령행이 파싱될 때 생성되므로, --init-goal 옵션이 argument_value/2argument_list/1을 사용하면 원래 명령행 인자(모든 인식된 인자 포함)를 얻게 돼요.

다음은 실행 목표 옵션을 사용한 예시예요:

% gprolog --init-goal 'write(before), nl' --entry-goal 'write(inside), nl' \
--query-goal 'append([a,b],[c,d],X)'

위는 다음을 생성해요:

before
GNU Prolog 1.5.0 (64 bits)
Compiled May 3 2021, 16:36:43 with gcc
Copyright (C) 1999-2021 Daniel Diaz
inside
| ?- append([a,b],[c,d],X).
X = [a,b,c,d]

yes
| ?-

참고: 사용 중인 셸에 따라 다른 문자열 구분자를 써야 할 수도 있어요(예: Windows cmd.exe에서는 " 사용).

대화형 인터프리터 읽기-실행-쓰기 루프

GNU Prolog top-level은 고전적인 읽기-실행-쓰기 루프 위에 구축되며, 질의가 결정적(deterministic)이지 않을 때 재실행도 허용해요. 그 과정은 다음과 같아요:

  1. 프롬프트, 즉 | ?-를 표시.
  2. 질의(즉 목표)를 읽음.
  3. 질의를 실행.
  4. 성공한 경우 질의의 변수 값들을 표시.
  5. 남은 대안이 있으면(즉 질의가 결정적이지 않으면) ?를 표시하고 사용자에게 다음 명령 중 하나를 묻고, RETURN은 실행을 멈추고, ;는 다음 해를 계산하며, a는 남은 모든 해를 계산.

다음은 질의 실행의 예시("X와 Y의 연결이 [a,b]인 리스트 X와 Y를 찾아라")예요:

| ?- append(X,Y,[a,b,c]).
X = []
Y = [a,b,c] ? ;          (사용자가 ; 를 눌러 다른 해 계산)
X = [a]
Y = [b,c] ? a            (사용자가 a 를 눌러 남은 모든 해 계산)
X = [a,b]
Y = [c]                  (묻지 않고 다음 해 계산)
X = [a,b,c]
Y = []                   (묻지 않고 다음 해 계산)

no                       (더 이상 해 없음)

어떤 경우 top-level은 현재 해가 마지막 해(남은 대안 없음)임을 감지할 수 있어요. 이 경우 ? 기호를 표시하지 않아요(사용자에게 묻지 않음). 예:

| ?- (X=1 ; X=2).
X = 1 ? ;                (사용자가 ; 를 눌러 다른 해 계산)
X = 2                    (더 이상 대안이 없으므로 프롬프트하지 않음)

yes

더 많은 대안이 있어도 사용자는 RETURN을 입력해 실행을 멈출 수 있어요:

| ?- (X=1 ; X=2).
X = 1 ?                  (사용자가 RETURN을 눌러 실행 정지)

yes

top-level은 질의의 변수 값들을 읽기 쉽게 표시하려고 해요. 예를 들어 변수가 질의 변수에 바인딩되면 그 변수의 이름이 나타나요. 변수가 싱글턴(singleton)이면 밑줄 기호 _가 표시돼요(_는 싱글턴 변수의 일반 이름이며 익명 변수라고도 불러요). 다른 변수들은 아예 새로운 변수 이름에 바인딩돼요.

질의 변수 이름 X가 다른 질의 변수 Y의 값으로 나타나는 것은 X 자체가 인스턴스화되지 않았기 때문이며, 그렇지 않으면 X의 값이 표시돼요. 이 경우 X 자체에 대해서는 (변수이므로) 아무것도 출력되지 않아요. 예:

| ?- X=f(A,B,_,A), A=k.
A = k                    (A의 값이 X에 대한 f/3 안에도 표시됨)
X = f(k,B,_,k)           (B는 X의 일부인 변수이므로 B가 표시되지 않음)
| ?- functor(T,f,3), arg(1,T,X), arg(3,T,X).
T = f(X,_,X)             (1번째, 3번째 인자가 X와 같고 2번째는 익명 변수)
| ?- read_from_atom('k(X,Y,X).',T).
T = k(A,_,A)             (1번째, 3번째 인자가 통일되고 새 변수 이름 A가 도입됨)

top-level은 변수 바인딩 술어를 사용해요(8.5절). 변수의 값을 표시하기 위해 top-level은 다음 옵션 목록으로 write_term/3을 호출해요: [quoted(true), numbervars(false), namevars(true)](8.14.6절).

'$VARNAME'(Name) 형태의 항(Name은 원자)은 변수 이름으로 표시되고, '$VAR'(N) 형태의 항(N은 정수)은 일반 복합 항으로 표시돼요(이런 항은 write_term/3에 의해 변수 이름으로 출력될 수도 있어요). 예:

| ?- X='$VARNAME'('Y'), Y='$VAR'(1).
X = Y                    (항 '$VARNAME'('Y')는 Y로 표시됨)
Y = '$VAR'(1)            (항 '$VAR'(1)은 그대로 표시됨)
| ?- X=Y, Y='$VAR'(1).
X = '$VAR'(1)
Y = '$VAR'(1)

첫 번째 예에서 X는 질의에 의해 '$VARNAME'('Y')에 명시적으로 바인딩되므로 top-level은 X의 값으로 Y를 표시해요. Y는 '$VAR'(1)과 통일되므로 일반 복합 항으로 표시돼요. X가 Y에 바인딩된 것이 아님은 분명해요(두 번째 질의에서는 그렇지만). 변수 바인딩 연산을 할 때 이 동작을 명심해야 해요.

마지막으로 top-level은 질의가 소요한 사용자 시간(user-time)(8.24.2절)을 계산하고, 유의미할 때 표시해요. 예:

| ?- retractall(p(_)), assertz(p(0)),
     repeat,
     retract(p(X)),
     Y is X + 1,
     assertz(p(Y)),
     X = 1000, !.
X = 1000
Y = 1001

(180 ms) yes             (질의가 180ms의 사용자 시간 소요)

Prolog 프로그램 조회하기(Consult)

top-level은 사용자가 Prolog 소스 파일을 조회(consult)할 수 있게 해줘요. 조회된 술어는 나열·실행·디버깅할 수 있어요(네이티브 코드로 컴파일된 술어는 불가능). 네이티브 코드 술어와 조회된 술어의 차이에 대한 자세한 내용은 이 절의 소개(4.1절)와 컴파일러 부분(4.4.1절)을 참고해요.

프로그램을 조회하려면 내장 술어 consult/1(8.23.1절)을 사용해요. 이 술어의 인자는 Prolog 파일 이름 또는 터미널을 지정하는 user예요. user를 쓰면 사용자가 터미널에서 직접 술어를 입력할 수 있어요. 이 경우 입력은 파일 끝(EOF) 키 시퀀스(Ctl-D)나 그 항 표현인 end_of_file으로 끝나야 해요. consult(FILE)의 약식은 [FILE]이에요. 예:

| ?- [user].
{compiling user for byte code...}
even(0).
even(s(s(X))):-
    even(X).
(여기서 사용자는 Ctl-D를 눌러 입력 종료)
{user compiled, 3 lines read - 350 bytes written, 1180 ms}
| ?- even(X).
X = 0 ? ;                (사용자가 ; 를 눌러 다른 해 계산)
X = s(s(0)) ? ;          (사용자가 ; 를 눌러 다른 해 계산)
X = s(s(s(s(0)))) ?      (사용자가 RETURN을 눌러 실행 정지)

yes
| ?- listing.
even(0).
even(s(s(A))) :-
    even(A).

consult/1(8.23.1절)이 Prolog 파일에 호출되면 먼저 GNU Prolog 컴파일러(4.4절)를 자식 프로세스로 실행해 바이트 코드용 임시 WAM 파일을 생성해요. 컴파일이 실패하면 메시지가 표시되고 아무것도 로드되지 않아요. 컴파일이 성공하면 생성된 파일이 load/1(8.23.2절)을 사용해 메모리에 로드돼요. 즉 각 술어의 바이트 코드가 로드돼요.

술어 P가 로드될 때 P의 이전 정의가 있으면 제거돼요(즉 P를 정의하는 모든 절이 지워져요). 이때 P가 재정의(redefined) 된다고 말해요. 조회된 술어만 재정의할 수 있다는 점을 주의하세요. P가 네이티브 코드 술어라면 재정의를 시도하면 로드 시점에 오류가 발생해요. 술어 재정의는 무시되고 다음 메시지가 표시돼요:

native code procedure P cannot be redefined

마지막으로, 기존 술어는 다시 로드되지 않으면 제거되지 않아요. 즉 파일 F를 조회할 때 술어 P가 로드되고, 나중에 P의 정의가 파일 F에서 제거된 후 F를 다시 조회해도 이전에 로드된 P의 정의는 메모리에서 제거되지 않아요.

조회된 술어는 Prolog 디버거를 사용해 디버깅할 수 있어요. 디버거를 활성화하려면 디버거 술어 trace/0debug/0(5.3.1절)을 사용해요.

Prolog 스크립팅

버전 1.4.0부터 Prolog 소스 파일을 Unix 스크립트 파일로 사용할 수 있어요(shebang 지원). PrologScript 파일은 다음과 같이 시작해야 해요:

#!/usr/bin/gprolog --consult-file

GNU Prolog는 /usr/bin/gprolog --consult-file FILE로 호출되고, 그다음 FILE이 조회돼요. #! 첫 줄을 올바르게 다루기 위해 consult/1#로 시작하는 파일의 첫 줄을 주석으로 취급해요(만약 #으로 시작하는 술어 이름을 쓰고 싶다면 그 정의 앞에 한 줄을 그냥 건너뛰면 돼요).

참고: 대부분의 시스템에서 shebang 구현이 #!/usr/bin/gprolog 뒤의 모든 인자를 하나의 문자열로 전달하므로(그래서 gprolog가 이를 올바르게 인식할 수 없음), 이런 방식으로 추가 매개변수(예: query-goal)를 전달하는 것은 거의 항상 불가능해요.

질의 중단하기

top-level에서 중단 키(Ctl-C)를 입력해 질의 실행을 중단할 수 있어요. 이는 질의를 중단(abort)하거나, 무한 루프를 멈추거나, 디버거를 활성화하는 데 쓰일 수 있어요. 중단이 발생하면 top-level은 다음 메시지를 표시해요:

Prolog interruption (h for help) ?

그러면 사용자는 다음 명령 중 하나를 입력할 수 있어요:

명령 이름 설명
a abort 현재 실행을 중단. abort/0(8.18.1절)과 동일
e exit 현재 Prolog 프로세스를 종료. halt/0(8.18.1절)과 동일
b break 재귀적 top-level을 호출. break/0(8.18.1절)과 동일
c continue 실행 재개
t trace trace/0(5.3.1절)로 디버거 시작
d debug debug/0(5.3.1절)로 디버거 시작
h 또는 ? help 사용 가능한 명령 요약 표시

라인 편집기

라인 편집기(linedit)는 사용자가 다양한 명령으로 현재 입력 줄을 만들고/수정할 수 있게 해줘요. 이 기능은 GNU Prolog의 linedit 부분이 설치되어 있을 때 사용할 수 있어요. linedit는 터미널에서 읽는 어떤 내장 술어(예: get_char/1, read/1 등)에 의해 암시적으로 호출돼요. top-level이 질의를 읽을 때도 마찬가지예요.

바인딩: linedit의 각 명령은 키로 활성화돼요. 일부 명령에는 다른 대체 키도 사용할 수 있어요(일부 터미널에서는 대체 키가 제대로 작동하지 않을 수 있지만 기본 키는 항상 작동해요). 사용 가능한 명령 목록은 다음과 같아요:

대체 키 설명
Ctl-B 이전 문자로 이동
Ctl-F 다음 문자로 이동
Esc-B Ctl-← 이전 단어로 이동
Esc-F Ctl-→ 다음 단어로 이동
Ctl-A Home 줄의 시작으로 이동
Ctl-E End 줄의 끝으로 이동
Ctl-H Backspace 이전 문자 삭제
Ctl-D Delete 현재 문자 삭제
Ctl-U Ctl-Home 줄 시작부터 현재 문자까지 삭제
Ctl-K Ctl-End 현재 문자부터 줄 끝까지 삭제
Esc-L 다음 단어를 소문자로
Esc-U 다음 단어를 대문자로
Esc-C 다음 단어를 대문자 시작으로
Ctl-T 마지막 두 문자 교환
Ctl-V Insert 삽입/대체 모드 전환
Ctl-I Tab 단어 완성(두 번 누르면 가능한 완성 모두 표시)
Esc-Ctl-I Esc-Tab 탭을 흉내 내도록 공백 삽입
Ctl-space 선택 시작 표시
Esc-W 복사(선택 시작부터 현재 문자까지)
Ctl-W 잘라내기(선택 시작부터 현재 문자까지)
Ctl-Y 붙여넣기
Ctl-P 이전 히스토리 줄 불러오기
Ctl-N 다음 히스토리 줄 불러오기
Esc-P 현재 접두사로 시작하는 이전 히스토리 줄 불러오기
Esc-N 현재 접두사로 시작하는 다음 히스토리 줄 불러오기
Esc-< Page Up 첫 히스토리 줄 불러오기
Esc-> Page Down 마지막 히스토리 줄 불러오기
Ctl-C 중단 신호 발생(4.2.5절)
Ctl-D 파일 끝 문자 발생(줄의 시작에서)
RETURN 줄 확정
Esc-? 사용 가능한 명령 요약 표시

히스토리: 한 줄이 입력되면(즉 RETURN으로 끝나면) linedit는 이를 history라는 내부 목록에 기록해요. 나중에 적절한 명령으로 히스토리 줄을 불러올 수 있고 필요에 따라 수정할 수 있어요. 주어진 접두사로 시작하는 히스토리 줄을 불러오는 것도 가능해요. 예를 들어 write로 시작하는 이전 줄을 불러오려면 write를 입력한 다음 Esc-P를 눌러요. 또 다른 Esc-P는 write로 시작하는 더 이전 줄을 불러와요.

완성: linedit의 또 다른 중요한 기능은 완성(completion) 기능이에요. linedit는 알려진 단어 목록을 유지하고 이를 사용해 단어의 접두사를 완성해요. 처음에 이 목록에는 모든 미리 정의된 원자와 사용 가능한 술어에 해당하는 원자가 들어 있어요. 시스템에 새 원자가 나타나면 이 목록이 동적으로 갱신돼요(top-level에서 읽히거나, 내장 술어로 생성되거나, 새로 조회된 술어와 연결되거나 등). 완성 키(Tab)를 누르면 linedit는 다음과 같이 동작해요:

  1. 현재 단어를 접두사로 사용.
  2. 이 접두사로 시작하는 목록의 모든 단어 수집.
  3. 일치하는 모든 단어의 공통 부분 중 가장 긴 것으로 현재 단어 완성.
  4. 둘 이상의 단어가 일치하면 경고음(beep) 발생(두 번째 Tab은 모든 가능성을 표시).

예:

| ?- argu                 (사용자가 Tab을 눌러 단어 완성)
| ?- argument_            (linedit가 argu를 argument_ 로 완성하고 경고음 발생)
(사용자가 다시 Tab을 눌러 가능한 완성 모두 보기)
argument_counter          (linedit가 3가지 가능한 완성을 표시)
argument_list
argument_value
| ?- argument_            (linedit가 입력 줄 다시 표시)
| ?- argument_c           (argument_counter를 선택하기 위해 사용자가 c 와 Tab 누름)
| ?- argument_counter      (linedit가 argument_counter 로 완성)

균형 맞추기: linedit는 사용자가 (소괄호/대괄호/중괄호) 괄호가 잘 균형 잡혔는지 확인하게 해줘요. 이를 위해 닫는 괄호 기호, 즉 ), ], }를 입력하면 linedit는 관련 여는 괄호, 즉 (, [, {를 찾아 그 일치를 보여주기 위해 일시적으로 커서를 그 위에 옮겨요.

사용자 정의: linedit의 동작은 LINEDIT라는 환경 변수로 제어할 수 있어요. 이 변수는 다음 부분 문자열을 포함할 수 있어요:

  • no - linedit를 비활성화(변수의 유일한 값이어야 함).
  • ansi=no - ANSI 이스케이프 시퀀스를 사용하지 않음(Unix 전용).
  • out=N - 출력에 파일 디스크립터 N 사용(Unix 전용).
  • gui=no - GUI 콘솔로 컴파일되었어도 텍스트 모드로 실행(Windows).
  • gui=silent - GUI 콘솔이 없으면 조용히 텍스트 모드로 실행(Windows).
  • cp=N - 코드 페이지 N 사용(Windows 텍스트 콘솔).
  • oem_put=no - 문자를 배출할 때 Char→Oem 변환을 사용하지 않음(Windows 텍스트 콘솔).
  • oem_get=no - 문자를 읽을 때 Oem→Char 변환을 사용하지 않음(Windows 텍스트 콘솔).

더 알아보기