Prolog의 '공포' 피하기: 순수 선언적 프로그래밍 가이드

Prolog는 현대 프로그래밍 환경에서 종종 반항적인 선택으로 여겨집니다. Prolog를 수용하는 이들에게 이 언어는 문제를 어떻게 계산할 것인가(how)가 아니라 해결책이 무엇인가(what)에 집중함으로써 문제를 해결하는 방법을 제공합니다. 그러나 많은 프로그래머들—특히 초보자들—은 Prolog의 가장 큰 강점을 앗아가고, 강력한 논리 엔진을 취약하고 명령형 스타일의 스크립트로 변질시키는 함정에 빠지곤 합니다.

진정으로 효과적인 Prolog를 작성하려면 특정 "공포"를 피해야 합니다. 즉, 결함이 있는 프로그램을 만들고, 암시적 의존성을 유발하며, 언어 고유의 일반성을 상실하게 만드는 안티패턴들입니다. 언어의 순수하고 단조로운(monotonic) 부분 집합을 준수함으로써, 개발자는 올바를 뿐만 아니라 가장 일반적인 쿼리에 대응할 수 있을 만큼 유연한 프로그램을 만들 수 있습니다.

첫 번째 공포: 해결책의 상실

A 프로그램은 크게 두 가지 방식으로 결함이 있을 수 있습니다. 잘못된 답을 보고하거나, 의도한 해결책을 보고하는 데 실패하는 것입니다. 잘못된 답을 보고하는 것도 문제지만, 유효한 해결책을 보고하지 못하는 것은 프로그램이 관계(relation)로서 갖는 유용성을 제한하기 때문에 종종 더 교활합니다.

"해결책 상실"의 주요 원인은 불순한(impure) 비단조적 구성 요소들입니다. 여기에는 다음이 포함됩니다:

  • 컷(cut) 연산자 (!/0)
  • if-then-else 구성 요소 ((->)/2)
  • var/1 술어

이러한 요소들이 사용될 때, 프로그램은 순수한 논리적 기술을 멈추게 됩니다. 선언적 대안은 깨끗한 데이터 구조, 제약 조건(예: dif/2), 그리고 프로그램의 논리적 무결성을 유지하기 위한 if_/3와 같은 특수 메타 술어(meta-predicates)를 사용하는 것입니다.

두 번째 공포: 전역 상태

초보자들은 종종 assertz/1retract/1과 같은 술어를 사용하여 전역 데이터베이스를 수정함으로써 Prolog를 명령형 언어처럼 다루고 싶은 유혹을 느낍니다. 이는 암시적 의존성을 유발합니다. 즉, 프로그램의 동작이 술어가 호출되는 순서에 의존하게 되지만, 이 의존성은 논리에 명시적으로 인코딩되지 않습니다.

만약 이러한 술어들이 의도하지 않은 순서로 실행된다면, 프로그램은 예기치 않게 실패하거나 불안정한 결과를 생성할 수 있습니다. 해결책은 전역 상태를 완전히 피하는 것입니다. 대신, 술어 인자를 사용하거나 세미컨텍스트 표기법(semicontext notation)을 사용하여 상태를 프로그램 전체에 "스레드(thread)"함으로써 의존성을 명시적이고 유지 관리 가능하게 만드십시오.

세 번째 공포: 불순한 출력

또 다른 흔한 실수는 Prolog의 최상위 수준(toplevel)이 보고하도록 허용하는 대신 시스템 터미널에 직접 답을 출력하는 것입니다 (예: format/2 사용).

% The Impure Way
solve :- 
    solution(S), 
    format("the solution is: ~q\n", [S]).

이 접근 방식은 매우 제한적입니다. 출력이 Prolog 용어(term)가 아니라 터미널의 문자열로만 존재하기 때문입니다. 이는 출력에 대한 자동화된 테스트 케이스를 작성하는 것을 거의 불가능하게 만들고, 코드가 진정한 관계(relation)로서 사용되는 것을 방지합니다.

올바른 접근 방식은 해결책을 기술하고 최상위 수준에서 표현을 처리하도록 하는 것입니다:

% The Pure Way
solution(S) :- 
    constraint_1(S), 
    etc.

네 번째 공포: 저수준 언어 구성 요소

많은 프로그래머들은 is/2, =:=/2, >/2와 같은 저수준 산술 술어를 고수합니다. 이러한 술어들은 수십 년 동안 업계에서 사용되어 왔지만, 프로그래머가 선언적 의미론과 실행적 의미론을 동시에 관리해야 하므로 인지적 부하를를 늘리고 언어를 배우고 가르치기 어렵게 만듭니다.

이러한 구성 요소들은 관계를 함수로 취급하며, 이는 Prolog의 본질에 대한 근본적인 오해입니다. 현대적인 대안은 유한 도메인에 대한 제약 논리 프로그래밍(Constraint Logic Programming over Finite Domains, CLP(FD))을 사용하는 것입니다. 이는 정수 산술에 대해 더 선언적 접근 방식을 가능하게 합니다.

사례 연구: 공포의 팩토리얼

이러한 점들을 설명하기 위해, 전형적인 "공포" 방식의 팩토리얼 구현을 살펴보겠습니다:

horror_factorial(0, 1) :- !.
horror_factorial(N, F) :- 
    N > 0, 
    N1 is N - 1, 
    horror_factorial(N1, F1), 
    F is N*F1.

이 버전은 여러 면에서 결함이 있습니다. 만약 가장 일반적인 쿼리(?- horror_factorial(N, F).)를 던진다면, 컷(!) 때문에 프로그램은 N=0, F=1 이외의 어떤 해결책도 찾지 못합니다. 만약 컷을 제거한다면, is/2가 인자를 인스턴스화(instantiated)할 것을 요구하기 때문에 프로그램은 여전히 instantiation_error로 실패할 것입니다.

CLP(FD)를 사용하여 순수하고 선언적인 버전으로 전환하면, 프로그램은 진정으로 일반적입니다:

n_factorial(0, 1).
n_factorial(N, F) :- 
    N #> 0, 
    N1 #= N - 1, 
    n_factorial(N1, F1), 
    F #= N*F1.

이제 ?- n_factorial(N, F). 쿼리는 N과 F의 가능한 모든 쌍(0,1; 1,1; 2,2; 3,6; 등등)을 올바르게 생성할합니다. 이로써 코드를 좁은 함수에서 강력한 수학적 관계(relation)로 변환합니다.

종합 및 관점

순수성을 추구하는 것은 논리적으로 타당하지만, 종종 실무자들 사이에서 논쟁을 불러일으킵니다. 가장자리 상황에 대한 경찰은 "과도하다"고 주장하며, 특정 실칙용 애플리케이션의 경우 명령형 단축키가 허용될 수 있다고 제안합니다. 다른 이들은 Prolog의 이해를 위해서는 four-port 모델과 같은 실행적 모델에 대한 더 깊은 이해가 필요하며, 그래야만 왜 이러한 안티패턴들이 문제가 되는지 진정으로 이해할 수 있다고 지적합니다.

커뮤니티 논의에서 얻은 흥미로운 시사점 중 하나는, 어떤 맥락에서는 나중에 필터링할 수 있는 몇몇 "잘못된" 답을 보고하는 것이, 정답을 완전히 놓치는 것보다 더 수용 가능할다는 점입니다. 이는 any 유효한 해결책을 찾는 것이 목표인 NP-완전 문제의 도전 과제와 일치하는 관점입니다.

Sources