【问题标题】:Why does my Prolog S-expression tokenizer fail on its base case?为什么我的 Prolog S 表达式标记器在其基本情况下失败?
【发布时间】:2020-12-16 22:58:57
【问题描述】:

为了学习一些 Prolog(我正在使用 GNU Prolog)并了解它的解析能力,我首先编写了一个 Lisp(或 S 表达式,如果我准确地说)标记器,它给出了一组标记,例如['(', 'f', 'o', 'o', ')'] 应该产生 ['(', 'foo', ')']。它没有按预期工作,这就是我在这里的原因!我认为我的思维过程在我的伪代码中闪耀:

tokenize([current | rest], buffer, tokens):
    if current is '(' or ')',
        Tokenize the rest,
        And the output will be the current token buffer,
        Plus the parenthesis and the rest.

    if current is ' ',
        Tokenize the rest with a clean buffer,
        And the output will be the buffer plus the rest.
    
    if the tail is empty,
        The output will be a one-element list containing the buffer.
    
    otherwise,
        Add the current character to the buffer,
        And the output will be the rest tokenized, with a bigger buffer.

我把它翻译成 Prolog 是这样的:

tokenize([Char | Chars], Buffer, Tokens) :-
    ((Char = '(' ; Char = ')') ->
        tokenize(Chars, '', Tail_Tokens),
        Tokens is [Buffer, Char | Tail_Tokens];
    Char = ' ' ->
        tokenize(Chars, '', Tail_Tokens),
        Tokens is [Buffer | Tail_Tokens];

    Chars = [] -> Tokens is [Buffer];

    atom_concat(Buffer, Char, New_Buffer),
    tokenize(Chars, New_Buffer, Tokens)).

print_tokens([]) :- write('.').
print_tokens([T | N]) :- write(T), write(', '), print_tokens(N).

main :-
    % tokenize(['(', 'f', 'o', 'o', '(', 'b', 'a', 'r', ')', 'b', 'a', 'z', ')'], '', Tokens),
    tokenize(['(', 'f', 'o', 'o', ')'], '', Tokens),
    print_tokens(Tokens).

运行结果时,如下所示:gprolog --consult-file lisp_parser.pl 它只是告诉我no。我跟踪了main,它给了我下面的堆栈跟踪。我不明白为什么 tokenize 会因为空壳而失败。我看到缓冲区是空的,因为它是用之前的')' 清除的,但是即使Tokens 在那个时间点是空的,Tokens 不会递归地累积更大的结果吗?有擅长Prolog的人可以在这里给我一些提示吗?

| ?- main.

no
| ?- trace.
The debugger will first creep -- showing everything (trace)

(1 ms) yes
{trace}
| ?- main.
      1    1  Call: main ? 
      2    2  Call: tokenize(['(',f,o,o,')'],'',_353) ? 
      3    3  Call: tokenize([f,o,o,')'],'',_378) ? 
      4    4  Call: atom_concat('',f,_403) ? 
      4    4  Exit: atom_concat('',f,f) ? 
      5    4  Call: tokenize([o,o,')'],f,_429) ? 
      6    5  Call: atom_concat(f,o,_454) ? 
      6    5  Exit: atom_concat(f,o,fo) ? 
      7    5  Call: tokenize([o,')'],fo,_480) ? 
      8    6  Call: atom_concat(fo,o,_505) ? 
      8    6  Exit: atom_concat(fo,o,foo) ? 
      9    6  Call: tokenize([')'],foo,_531) ? 
     10    7  Call: tokenize([],'',_556) ? 
     10    7  Fail: tokenize([],'',_544) ? 
      9    6  Fail: tokenize([')'],foo,_519) ? 
      7    5  Fail: tokenize([o,')'],fo,_468) ? 
      5    4  Fail: tokenize([o,o,')'],f,_417) ? 
      3    3  Fail: tokenize([f,o,o,')'],'',_366) ? 
      2    2  Fail: tokenize(['(',f,o,o,')'],'',_341) ? 
      1    1  Fail: main ? 

(1 ms) no
{trace}
| ?- 

【问题讨论】:

  • 您可能需要为此使用定句语法,它用于处理(字符或其他事物的)列表。但除此之外,这很奇怪:Tokens is [Buffer, Char | Tail_Tokens]; ... is 是算术评估,这可能不是您想要的。你想要atom_concat(Buffer,Char,Token), Tokens = [Token|Tail_Tokens]吗?
  • @DavidTonhofer 我在想,如果有括号,我会做这样的事情:'a', 'b', '(', 'c' 对于那个 sn-p,一旦标记器命中括号,它就会知道缓冲区 @ 987654336@ 是一个完整的令牌,它可以清除令牌缓冲区。那么Tokens 将部分是['ab', '(', ...]。你能解释一下为什么我要把括号和ab放在一起吗?什么是算术评估?我是新手,所以你可能知道得更好。
  • 如果您调用is,那么is 右侧的内容必须是算术表达式。如X is 4+5。但是你说Tokens is [Buffer, Char | Tail_Tokens]。应该是Tokens = [Buffer, Char | Tail_Tokens],统一就好。
  • 除了将is 替换为=,您还必须为递归定义添加一个基本情况:tokenize([], _, []).

标签: recursion prolog tokenize s-expression


【解决方案1】:

这个怎么样。我认为这就是您想要做的,但是让我们使用 Definite Clause Grammars(它们只是将 :- 替换为 --> 的角子句以及两个包含输入字符列表和剩余字符列表的省略参数. DCG 规则示例:

rule(X) --> [c], another_rule(X), {predicate(X)}.

列表处理规则rule//1说:当你在输入列表中找到字符c,然后用another_rule//1继续列表处理,当成功时,照常调用predicate(X)

然后:

% If we encounter a separator symbol '(' or ')', we commit to the
% clause using '!' (no point trying anything else, in particular
% not the clause for "other characters", tokenize the rest of the list,
% and when we have done that decide whether 'MaybeToken', which is 
% "part of the leftmost token after '(' or ')'", should be retained.
% it is dropped if it is empty. The caller is then given an empty
% "part of the leftmost token" and the list of tokens, with '(' or ')'
% prepended: "tokenize('', [ '(' | MoreTokens] )  -->"
 
tokenize('', [ '(' | MoreTokens] ) -->
   ['('],
   !,
   tokenize(MaybeToken,Tokens),
   {drop_empty(MaybeToken,Tokens,MoreTokens)}.
   
tokenize('',[')'|MoreTokens]) --> 
   [')'],
   !,
   tokenize(MaybeToken,Tokens),
   {drop_empty(MaybeToken,Tokens,MoreTokens)}.
   
% No more characters in the input list (that's what '--> []' says).
% We succeed, with an empty token list and an empty buffer fro the
% leftmost token.

tokenize('',[]) --> [].

% If we find a 'Ch' that is not '(' or ')', then tokenize
% more of the list via 'tokenize(MaybeToken,Tokens)'. On
% returns 'MaybeToken' is a piece of the leftmost token found
% in that list, so we have to stick 'Ch' onto its start.

tokenize(LargerMaybeToken,Tokens) --> 
   [Ch],
   tokenize(MaybeToken,Tokens),
   {atom_concat(Ch,MaybeToken,LargerMaybeToken)}.

% ---
% This drops an empty "MaybeToken". If "MaybeToken" is 
% *not* empty, it is actually a token and prepended to the list "Tokens"
% ---

drop_empty('',Tokens,Tokens) :- !.
drop_empty(MaybeToken,Tokens,[MaybeToken|Tokens]).

% -----------------
% Call the DCG using phrase/2
% -----------------

tokenize(Text,Result) :-
   phrase( tokenize(MaybeToken,Tokens), Text ),
   drop_empty(MaybeToken,Tokens,Result),!.

所以:

?- tokenize([h,e,l,l,o],R).
R = [hello].

?- tokenize([h,e,l,'(',l,')',o],R).
R = [hel,(,l,),o].

?- tokenize([h,e,l,'(',l,l,')',o],R).
R = [hel,(,ll,),o].

我认为在 GNU Prolog 中,符号 `hello` 直接生成 [h,e,l,l,o]

【讨论】:

  • 您的代码有效,但我无法全部理解(我想这只是我使用 Prolog 的第三天)。在许多tokenizes 之前有一个空原子 - 为什么会这样?还有,! 在这里表示什么?这让我有点困惑。顺便说一句,谢谢您的详尽回答。
  • @CaspianAhlberg 这需要一点时间来适应。这 ”!”说 Prolog 应该“承诺”到当前选择的子句。如果谓词“to left”有任何失败,因此发生“redo”(搜索其他解决方案),执行将不会从右到左执行“!”或其他具有相同名称/数量的子句将不会被尝试。相反,整个谓词将被视为失败并选择另一个谓词。
  • (我试图在 Byrd Box 模型上写一个 not 来解释 Prolog 的执行,也许这会有所帮助:Byrd Box Model。它可能有一些错误。
  • tokenize//2 中的“空原子”是缓冲区状态。在头部('-->' 左侧)是告诉调用者的内容。这就是为什么当我们遇到'('时,左侧缓冲区是空的。但是,如果我们遇到'Ch',那么要告诉调用者的缓冲区状态就是我们通过递归调用得到的缓冲区状态,左侧有“Ch”。
【解决方案2】:

我不明白为什么 tokenize 会因为空壳而失败。

Prolog 中任何事情都失败的原因是因为没有子句使它成为真的。如果tokenize 的唯一子句是tokenize([Char | Chars], ...) 形式,那么tokenize([], ...) 形式的调用将永远无法匹配此子句,并且由于没有其他子句,调用将失败。

所以你需要添加这样一个子句。但首先:

:- set_prolog_flag(double_quotes, chars).

这允许您将['(', f, o, o, ')'] 写为"foo"

此外,您必须计划输入完全为空的情况,或者您必须为缓冲区发出令牌的其他情况,但前提是它不是''(因为不应该有''标记乱扔结果)。

finish_buffer(Tokens, Buffer, TokensMaybeWithBuffer) :-
    (   Buffer = ''
    ->  TokensMaybeWithBuffer = Tokens
    ;   TokensMaybeWithBuffer = [Buffer | Tokens] ).

例如:

?- finish_buffer(MyTokens, '', TokensMaybeWithBuffer).
MyTokens = TokensMaybeWithBuffer.

?- finish_buffer(MyTokens, 'foo', TokensMaybeWithBuffer).
TokensMaybeWithBuffer = [foo|MyTokens].

请注意,您可以将缓冲区添加到标记列表中,即使您还不知道标记列表是什么!这就是逻辑变量的力量。其余代码也使用这种技术。

所以,空输入的情况:

tokenize([], Buffer, Tokens) :-
    finish_buffer([], Buffer, Tokens).

例如:

?- tokenize([], '', Tokens).
Tokens = [].

?- tokenize([], 'foo', Tokens).
Tokens = [foo].

还有剩下的情况:

tokenize([Parenthesis | Chars], Buffer, TokensWithParenthesis) :-
    (   Parenthesis = '('
    ;   Parenthesis = ')' ),
    finish_buffer([Parenthesis | Tokens], Buffer, TokensWithParenthesis),
    tokenize(Chars, '', Tokens).
tokenize([' ' | Chars], Buffer, TokensWithBuffer) :-
    finish_buffer(Tokens, Buffer, TokensWithBuffer),
    tokenize(Chars, '', Tokens).
tokenize([Char | Chars], Buffer, Tokens) :-
    Char \= '(',
    Char \= ')',
    Char \= ' ',
    atom_concat(Buffer, Char, NewBuffer),
    tokenize(Chars, NewBuffer, Tokens).

注意我是如何对不同的情况使用不同的子句的。这使代码更具可读性,但与(... -> ... ; ...) 相比,它确实有一个缺点,即最后一个子句必须排除前面子句处理的字符。一旦您的代码采用这种形状,并且您很高兴它可以工作,您可以使用(... -> ... ; ...) 将其转换为一种形式,如果您真的想要的话。

例子:

?- tokenize("(foo)", '', Tokens).
Tokens = ['(', foo, ')'] ;
false.

?- tokenize(" (foo)", '', Tokens).
Tokens = ['(', foo, ')'] ;
false.

?- tokenize("(foo(bar)baz)", '', Tokens).
Tokens = ['(', foo, '(', bar, ')', baz, ')'] ;
false.

最后,非常重要is 运算符用于计算算术表达式。当您将其应用于任何非算术运算时,它将引发异常。统一不同于算术表达式的求值。统一写为=

?- X is 2 + 2.
X = 4.

?- X = 2 + 2.
X = 2+2.

?- X is [a, b, c].
ERROR: Arithmetic: `[a,b,c]' is not a function
ERROR: In:
ERROR:   [20] throw(error(type_error(evaluable,...),_3362))
ERROR:   [17] arithmetic:expand_function([a,b|...],_3400,_3402) at /usr/lib/swi-prolog/library/arithmetic.pl:175
ERROR:   [16] arithmetic:math_goal_expansion(_3450 is [a|...],_3446) at /usr/lib/swi-prolog/library/arithmetic.pl:147
ERROR:   [14] '$expand':call_goal_expansion([system- ...],_3512 is [a|...],_3492,_3494,_3496) at /usr/lib/swi-prolog/boot/expand.pl:863
ERROR:   [13] '$expand':expand_goal(_3566 is [a|...],_3552,_3554,_3556,user,[system- ...],_3562) at /usr/lib/swi-prolog/boot/expand.pl:524
ERROR:   [12] setup_call_catcher_cleanup('$expand':'$set_source_module'(user,user),'$expand':expand_goal(...,_3640,_3642,_3644,user,...,_3650),_3614,'$expand':'$set_source_module'(user)) at /usr/lib/swi-prolog/boot/init.pl:443
ERROR:    [8] '$expand':expand_goal(user:(_3706 is ...),_3692,user:_3714,_3696) at /usr/lib/swi-prolog/boot/expand.pl:458
ERROR:    [6] setup_call_catcher_cleanup('$toplevel':'$set_source_module'(user,user),'$toplevel':expand_goal(...,...),_3742,'$toplevel':'$set_source_module'(user)) at /usr/lib/swi-prolog/boot/init.pl:443
ERROR: 
ERROR: Note: some frames are missing due to last-call optimization.
ERROR: Re-run your program in debug mode (:- debug.) to get more detail.
^  Call: (14) call('$expand':'$set_source_module'(user)) ? abort
% Execution Aborted
?- X = [a, b, c].
X = [a, b, c].

【讨论】:

    猜你喜欢
    • 2021-03-06
    • 1970-01-01
    • 2019-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多