在SWI-Prolog中编写宏

8

我正在尝试在 SWI-Prolog 实现一个简单的 switch 语句宏。

这是一系列条件语句:

(X = a ->
    Output = case1;
X = b ->
    Output = case2;
X = c ->
    Output = case3).

这是一种等效但速度较慢的表达式,具有相同的效果:

switch(X, [
    a : (Output = case1),
    b : (Output = case2),
    c : (Output = case3)
])

我在一个应用程序中使用了许多类似这样的谓词,但这会显著地减慢它的速度。是否有可能将这个开关谓词实现为宏,在编译时将其转换为普通的条件表达式,以提高应用程序的性能?


3
最快的方法可能不是一系列if-then-else,而是为每种情况设置一组子句。额外的好处是:您可以在更多方向上使用这些子句。请查看term_expansion/2goal_expansion/2。以在编译时重写术语。请参阅SWI-Prolog的apply_macros库中的maplist/2,了解如何将此类结构编译为辅助谓词的调用。 - mat
已经有一种方法可以完全实现您描述的内容:使用具有多个子句的谓词,其中第一个参数是“开关”表达式(正如@mat所指出的那样...)。为什么要放弃这种惯用的、广泛使用的结构呢? - user1812457
PS. 如果问题实际上是关于如何在编译时进行扩展,那么这确实是一个不同的问题。 - user1812457
@Boris 是的,这就是问题所在。 - Anderson Green
2个回答

4
一个最基本的尝试:创建一个名为switch.pl的文件。
:- module(switch, []).

compile_caselist(X, [K:Clause], (X = K -> Clause)) :- !.
compile_caselist(X, [K:Clause|CaseList], ((X = K -> Clause);Translated)) :-
    compile_caselist(X, CaseList, Translated).

:- multifile user:goal_expansion/2.
user:goal_expansion(F, G) :-
    F = switch(X, CaseList),
    compile_caselist(X, CaseList, G).

然后像往常一样使用它:例如,在文件switch_test.pl中。

:- use_module(switch).

test1(X) :-
    X = a -> writeln(case1) ;
    X = b -> writeln(case2) ;
    X = c -> writeln(case3).

test2(X) :-
    switch(X, [
           a : writeln(case1),
           b : writeln(case2),
           c : writeln(case3)
       ]).

编译 switch_test.pl 后:

?- listing(test2).
test2(A) :-
    (   A=a
    ->  writeln(case1)
    ;   A=b
    ->  writeln(case2)
    ;   A=c
    ->  writeln(case3)
    ).

true.

由于多次请求,这里提供一个编译模式来分离条款:
:- module(switch, []).

:- multifile user:term_expansion/2.
user:term_expansion((H:-B), [(H:-T)|SWs]) :-
    collect_switches(H,B,T,SWs),
    SWs \= [],
    debug(switch, 'compiled <~w>~nto <~w>~nwith <~w>', [H,T,SWs]).

collect_switches(H,(A0;A),(B0;B),SWs) :-
    collect_switches(H,A0,B0,S0),
    collect_switches(H,A,B,S),
    append(S0,S,SWs).

collect_switches(H,(A0,A),(B0,B),[S|SWs]) :-
    call_switch(H,A0,B0,S), !,
    collect_switches(H,A,B,SWs).
collect_switches(H,(A0,A),(A0,B),SWs) :-
    collect_switches(H,A,B,SWs).
collect_switches(H,A,B,[S]) :-
    call_switch(H,A,B,S), !.
collect_switches(_,C,C,[]).

call_switch(H,switch(X,CL),call(G,X),CTs) :-
    functor(H,F,A),
    R is random(1000000),
    format(atom(G), '~s_~d_~d', [F,A,R]),
    maplist({G}/[K:C,(H:-C)]>>(H=..[G,K]),CL,CTs).

现在测试脚本已经被封装在一个模块中,以便于进一步的列表显示:

:- module(switch_test, [test1/1,test2/1]).
:- use_module(switch).

test1(X) :-
    X = a -> writeln(case1) ;
    X = b -> writeln(case2) ;
    X = c -> writeln(case3).

test2(X) :-
    switch(X, [
           a : writeln(case1),
           b : writeln(case2),
           c : writeln(case3)
       ]).

编译完switch_test.pl后的结果如下:

?- switch_test:listing.

test1(A) :-
    (   A=a
    ->  writeln(case1)
    ;   A=b
    ->  writeln(case2)
    ;   A=c
    ->  writeln(case3)
    ).

test2(A) :-
    call(test2_1_362716, A).

test2_1_362716(a) :-
    writeln(case1).
test2_1_362716(b) :-
    writeln(case2).
test2_1_362716(c) :-
    writeln(case3).

为了方便调试:
?- debug(switch).

编译时输出如下的提示信息:

% [Thread pq] compiled <test2(_G121946)>
to <call(test2_1_362716,_G121946)>
with <[[(test2_1_362716(a):-writeln(case1)),(test2_1_362716(b):-writeln(case2)),(test2_1_362716(c):-writeln(case3))]]>

注意:这个草图显然需要更多的测试。

如果您决定对改进进行基准测试(如果有),请不要使用IO语句(例如writeln),因为那些会主宰执行时间。


这仍然看起来非常浪费。为什么不扩展到单个子句呢? - user1812457
@Boris:编译到单独子句存在问题,我认为...例如,传递上下文或构思一个合适的唯一名称。当然可以做到,但不能简短回答,主要是针对OP(非常精确)的问题。 - CapelliC
@CapelliC,是否有可能将switch([a:b,c:d]).自动扩展为多个子句,如case(a,b). case(c,d). - Anderson Green
@AndersonGreen 可以做到,但我认为这将是另一个问题。您需要解释为什么将其编写为 case(a,b). case(c,d) 不好。您可以使用 term_hash/2 创建名称(请阅读链接底部的注释!并查看 library(apply_macros) 的实现)。 - user1812457
非常好的工作,感谢您发布这个!collect_switches/4似乎是使用DCG的一个很好的候选项。 - mat
使用Prolog的部分求值器也可能解决这个问题。 - Anderson Green

2

我希望你只是为了演示而使用writeln。这是编写与你问题相同的程序的惯用方式:

foo(a, case1).
foo(b, case2).
foo(c, case3).

这个程序的功能如下:

?- foo(a, X).
X = case1.

?- foo(X, case1).
X = a.

?- foo(X, Y).
X = a,
Y = case1 ;
X = b,
Y = case2 ;
X = c,
Y = case3.

重要提示:

  • 不需要使用writeln,顶层已经处理了输出(如果您确实需要输出,则可以这样做,但最好将其与其它逻辑分开)。
  • 这肯定比其他任何建议更节省空间和时间。
  • 当switch表达式是变量时,您可以枚举您的情况。

您是否可能没有完全理解这个回答中关于您提出的问题的内容?

请注意,如果您可以在谓词头中完成所有操作,则甚至不需要谓词体:再次查看相同的答案和我的示例。

您似乎因为参数数量而放弃了这个建议,但我没看出任何其他解决方案如何解决这个问题。您能否在问题中演示当涉及到更多参数时您想要编写的switch语句?

还有一件事:如果您有很多cases,那么将它们写在列表中可能会更容易;然后,您可以使用术语扩展在编译时向数据库添加表格。 请参见这个问题这个答案末尾的term_expansion示例;该示例是从SWI-Prolog文档中抄录的(请看该页面底部)。您当然也可以使用goal_expansion而不是term_expansion


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接