C에서 Prolog 호출

C에서 Prolog 호출

C 함수에서 Prolog 술어를 호출(콜백)하는 방법을 설명해요.

출처: GNU Prolog Manual - Calling Prolog from C

본문

소개

다음 함수들은 C 함수가 Prolog 술어를 호출할 수 있게 해줘요:

void Pl_Query_Begin (PlBool recoverable)
int Pl_Query_Call (int functor, int arity, PlTerm *arg)
int Pl_Query_Start (int functor, int arity, PlTerm *arg, PlBool recoverable)
int Pl_Query_Next_Solution(void)
void Pl_Query_End (int op)
PlTerm Pl_Get_Exception (void)
void Pl_Exec_Continuation (int functor, int arity, PlTerm *arg)
void Pl_Throw (PlTerm ball)

Prolog 술어의 호출은 다음과 같이 해야 해요:

  1. Pl_Query_Begin()을 사용해 쿼리를 염.
  2. Pl_Query_Call()을 사용해 첫 해를 계산.
  3. 겨우 Pl_Query_Next_Solution()을 사용해 다음 해를 계산.
  4. Pl_Query_End()를 사용해 쿼리를 닫음.
  • Pl_Query_Begin(recoverable) — 쿼리를 초기화하는 데 사용. 인자 recoverable은 사용자가 쿼리 끝에서 쿼리가 소비한 메모리 공간을 회수하려면 PL_TRUE로 설정해야 해요(그 경우 추가 선택점이 생성됨). Pl_Query_Begin() 호출 후 Pl_Mk_... 계열 함수(10.4.5절)로 힙에 생성된 모든 항은 Pl_Query_End(PL_TRUE)를 호출할 때 회수될 수 있어요.
  • Pl_Query_Call(functor, arity, arg) — 인자를 전달하며 술어를 호출. 첫 해를 계산하는 데 사용. 인자 functor, arity, arg는 복합 항을 처리하는 함수들의 인자와 비슷해요(10.4.1절). 이 함수는 반환해요:
    • PL_FAILURE(PL_FALSE, 즉 0과 같은 상수) — 쿼리가 실패하면.
    • PL_SUCCESS(PL_TRUE, 즉 1과 같은 상수) — 성공 시. 그 경우 인자 배열 arg를 사용해 쿼리가 수행한 통일을 얻을 수 있어요.
    • PL_EXCEPTION(2와 같은 상수) — 그 경우 Pl_Get_Exception() 함수를 사용해 throw/1(7.2.4절)이 발생시킨 예외 항을 얻을 수 있어요.
  • Pl_Query_Start(functor, arity, arg, recoverable)Pl_Query_Begin(recoverable) 호출 뒤 Pl_Query_Call(functor, arity, arg) 호출이 뒤따르는 것과 동일한 축약형.
  • Pl_Query_Next_Solution() — 새 해를 계산하는 데 사용. 이전 해의 결과가 PL_SUCCESS였을 때만 사용해야 해요. 이 함수는 Pl_Query_Call()과 같은 종류의 값을 반환해요(위 참조).
  • Pl_Query_End(op) — 쿼리를 끝내는 데 사용. 이 함수는 주로 쿼리의 남은 대안을 관리해요. 그러나 쿼리에 대안이 없어도 이 함수를 사용해 쿼리를 올바르게 끝내야 해요. op의 값은:
    • PL_RECOVER — 쿼리가 소비한 메모리 공간을 회수. 그 후 Prolog 스택의 상태는 쿼리를 열기 전과 정확히 같아요. 이 옵션을 사용하려면 쿼리가 recoverable에 대해 PL_TRUE를 지정해 초기화되어야 해요(위 참조).
    • PL_CUT — 남은 대안을 컷. 이 옵션의 효과는 쿼리 뒤의 컷과 비슷해요.
    • PL_KEEP_FOR_PROLOG — Prolog를 위해 대안을 유지. 이는 쿼리가 foreign C 함수에서 호출됐을 때 유용해요. 그 경우 C foreign 함수에 대응하는 술어가 호출될 때 쿼리가 실행되고 남은 대안이 그 술어의 대안으로 사용 가능해요.

참고로 쿼리 스택이 유지되므로 여러 쿼리를 중첩할 수 있어요. 예를 들어 쿼리를 호출하고 끝내기 전에 다른 쿼리를 호출하는 것이 가능해요. 그 경우 첫 번째 Pl_Query_End() 실행이 두 번째(내부) 쿼리를 끝내고 다음 Pl_Query_End() 실행이 첫 번째 쿼리를 끝내요.

  • Pl_Exec_Continuation(functor, arity, arg) — 현재 계산을 지정된 술어의 실행으로 대체. 인자 functor, arity, arg는 복합 항을 처리하는 함수들의 인자와 비슷해요(10.4.1절).
  • Pl_Throw(ball) — 예외를 던짐. 예외에 대한 자세한 내용은 throw/1 제어 구조(7.2.4절)를 참조. 참고로 Pl_Throw(ball)Pl_Exec_Continuation(Pl_Find_Atom("throw"), 1, &ball)와 논리적으로 동일하지만(더 빠름).

예제: my_call/1 - call/1 복제본

여기에서 call(Goal)처럼 동작하지만 예외를 처리하지 않는 술어 my_call(Goal)을 정의해 봅시다(예외가 발생하면 목표는 단순히 실패).

Prolog 파일 examp.pl:

:- foreign(my_call(term)).

C 파일 examp_c.c:

#include <string.h>
#include <gprolog.h>

PlBool
my_call(PlTerm goal)
{
    PlTerm *arg;
    int functor, arity;
    int result;
    arg = Pl_Rd_Callable_Check(goal, &functor, &arity);
    Pl_Query_Begin(PL_FALSE);
    result = Pl_Query_Call(functor, arity, arg);
    Pl_Query_End(PL_KEEP_FOR_PROLOG);
    return (result == PL_SUCCESS);
}

사용 예:

| ?- my_call(write(hello)).
hello
| ?- my_call(for(X,1,3)).
X = 1 ? (여기서 사용자가 ;를 눌러 다른 해를 계산)
X = 2 ? (여기서 사용자가 ;를 눌러 다른 해를 계산)
X = 3 (더 이상 대안이 없으므로 사용자에게 묻지 않음)
| ?- my_call(1).
{exception: error(type_error(callable,1),my_call/1)}
| ?- my_call(call(1)).
no

my_call(1)이 호출되면 Pl_Rd_Callable_Check() 사용으로 인해 오류가 발생해요. 그러나 my_call(call(1))이 발생시킨 오류는 무시되고 foreign 함수에 의해 PL_FALSE(즉 실패)가 반환돼요.

예외가 회수될 때 call/1의 동작을 정말로 시뮬레이션하려면 더 이른 핸들러가 포착하도록 다시 발생시켜야 해요. 아이디어는 그런 다음 연속(continuation)으로 throw/1을 실행하는 것이에요. 다음 코드가 그렇게 해요:

#include <string.h>
#include <gprolog.h>

PlBool
my_call(PlTerm goal)
{
    PlTerm *args;
    int functor, arity;
    int result;
    args = Pl_Rd_Callable_Check(goal, &functor, &arity);
    Pl_Query_Begin(PL_FALSE);
    result = Pl_Query_Call(functor, arity, args);
    Pl_Query_End(PL_KEEP_FOR_PROLOG);
    if (result == PL_EXCEPTION)
    {
        PlTerm except = Pl_Get_Exception();
        Pl_Throw(except);
        // Pl_Exec_Continuation(Find_Atom("throw"), 1, &except)와 동일
    }
    return result;
}

다음 코드는 call/1이 발생시킨 오류를 전파해요:

| ?- my_call(call(1)).
{exception: error(type_error(callable,1),my_call/1)}

마지막으로 my_call/1을 정의하는 더 단순한 방법은 Pl_Exec_Continuation()을 다음과 같이 사용하는 것임을 참고해요:

#include <string.h>
#include <gprolog.h>

PlBool
my_call(PlTerm goal)
{
    PlTerm *args;
    int functor, arity;
    args = Pl_Rd_Callable_Check(goal, &functor, &arity);
    Pl_Exec_Continuation(functor, arity, args);
    return PL_TRUE;
}

예제: 모든 연산자의 리스트 회수

여기에서 List를 findall(X,current_op(_,_,X),List)이 하는 것처럼 현재 정의된 모든 연산자의 리스트와 통일하는 술어 all_op(List)를 정의해 봅시다.

Prolog 파일 examp.pl:

:- foreign(all_op(term)).

C 파일 examp_c.c:

#include <string.h>
#include <gprolog.h>

PlBool
all_op(PlTerm list)
{
    PlTerm op[1024];
    PlTerm args[3];
    int n = 0;
    int result;
    Pl_Query_Begin(PL_TRUE);
    args[0] = Pl_Mk_Variable();
    args[1] = Pl_Mk_Variable();
    args[2] = Pl_Mk_Variable();
    result = Pl_Query_Call(Find_Atom("current_op"), 3, args);
    while (result)
    {
        op[n++] = Pl_Mk_Atom(Pl_Rd_Atom(args[2])); /* arg[2]: op의 이름 */
        result = Pl_Query_Next_Solution();
    }
    Pl_Query_End(PL_RECOVER);
    return Pl_Un_Proper_List_Check(n, op, list);
}

여기에서는 예외의 원천이 없음을 알고 있다는 점을 참고해요. 그 경우 Pl_Query_Call과 Pl_Query_Next_Solution의 결과는 불리언으로 간주될 수 있어요.

사용 예:

| ?- all_op(L).
L = [:-,:-, \=,=:=,#>=,#<#,@>=,-->,mod,#>=#,**,*,+,+,',',...]
| ?- findall(X,current_op(_,_,X),L).
L = [:-,:-, \=,=:=,#>=,#<#,@>=,-->,mod,#>=#,**,*,+,+,',',...]

더 알아보기