Pure Prolog Scheme Quine

前端 未结 3 644
走了就别回头了
走了就别回头了 2021-01-21 06:15

There is this paper:

William E. Byrd, Eric Holk, Daniel P. Friedman, 2012
miniKanren, Live and Untagged
Quine Generation via Relational Interpret

相关标签:
3条回答
  • 2021-01-21 06:29

    Here is a solution that uses a little blocking programming style. Its not using when/2, rather only freeze/2. There is one predicate expr/2 which checks whether something is a proper expression without any closure in it:

    expr(X) :- freeze(X, expr2(X)).
    
    expr2([X|Y]) :-
       expr(X),
       expr(Y).
    expr2(quote).
    expr2([]).
    expr2(cons).
    expr2(lambda).
    expr2(symbol(_)).
    

    And then there is a lookup predicate again using freeze/2,
    to wait for an environment list.

    lookup(S, E, R) :- freeze(E, lookup2(S, E, R)).
    
    lookup2(S, [S-T|_], R) :-
       unify_with_occurs_check(T, R).
    lookup2(S, [T-_|E], R) :-
       dif(S, T),
       lookup(S, E, R).
    

    And finally the evaluator, which is coded using DCG,
    to limit the total number of cons and apply invokations:

    eval([quote,X], _, X) --> [].
    eval([], _, []) --> [].
    eval([cons,X,Y], E, [A|B]) -->
       step,
       eval(X, E, A),
       eval(Y, E, B).
    eval([lambda,symbol(X),B], E, closure(X,B,E)) --> [].
    eval([X,Y], E, R) -->
       step,
       eval(X, E, closure(Z,B,F)),
       eval(Y, E, A),
       eval(B, [Z-A|F], R).
    eval(symbol(S), E, R) -->
       {lookup(S, E, R)}.
    
    step, [C] --> [D], {D > 0, C is D-1}.
    

    The main predicate gradually increases the number of allowed
    cons and apply invokations:

    quine(Q, M, N) :-
       expr(Q),
       between(0, M, N),
       eval(Q, [], P, [N], _),
       unify_with_occurs_check(Q, P).
    

    This query shows that 5 cons and apply invokations are enough to produce a Quine. Works in SICStus Prolog and Jekejeke Prolog. For SWI-Prolog need to use for example this unify/2 workaround:

    ?- dif(Q, []), quine(Q, 6, N).
    Q = [[lambda, symbol(_Q), [cons, symbol(_Q), [cons, [cons, 
    [quote, quote], [cons, symbol(_Q), [quote, []]]], [quote, 
    []]]]], [quote, [lambda, symbol(_Q), [cons, symbol(_Q), [cons, 
    [cons, [quote, quote], [cons, symbol(_Q), [quote, []]]], 
    [quote, []]]]]]],
    N = 5 
    

    We can manually verify that it is indeed a non-trivial Quine:

    ?- Q = [[lambda, symbol(_Q), [cons, symbol(_Q), [cons, [cons, 
    [quote, quote], [cons, symbol(_Q), [quote, []]]], [quote, 
    []]]]], [quote, [lambda, symbol(_Q), [cons, symbol(_Q), [cons, 
    [cons, [quote, quote], [cons, symbol(_Q), [quote, []]]], 
    [quote, []]]]]]], eval(Q, [], P, [5], _).
    Q = [[lambda, symbol(_Q), [cons, symbol(_Q), [cons, [cons, 
    [quote, quote], [cons, symbol(_Q), [quote, []]]], [quote, 
    []]]]], [quote, [lambda, symbol(_Q), [cons, symbol(_Q), [cons, 
    [cons, [quote, quote], [cons, symbol(_Q), [quote, []]]], 
    [quote, []]]]]]],
    P = [[lambda, symbol(_Q), [cons, symbol(_Q), [cons, [cons, 
    [quote, quote], [cons, symbol(_Q), [quote, []]]], [quote, 
    []]]]], [quote, [lambda, symbol(_Q), [cons, symbol(_Q), [cons, 
    [cons, [quote, quote], [cons, symbol(_Q), [quote, []]]], 
    [quote, []]]]]]] 
    
    0 讨论(0)
  • 2021-01-21 06:34

    I'm using SWI Prolog with the occurs check turned on here (but dif/2 skips the occurs check anyway):

    symbol(X) :- freeze(X, atom(X)).
    
    symbols(X) :- symbol(X).
    
    symbols([]).
    
    symbols([H|T]) :-
        symbols(H),
        symbols(T).
    
    % lookup(X, Env, Val).
    %
    % [quote-unbound(quote)] will be the empty environment
    % when unbound(quote) is returned, this means that
    % `quote` is unbound
    
    lookup(X, [X-Val|_], Val).
    
    lookup(X, [Y-_|Tail], Val) :- 
        dif(X, Y),
        lookup(X, Tail, Val).
    
    % to avoid name clashing with `eval`
    %
    % evil(Expr, Env, Val).
    
    evil([quote, X], Env, X) :-
        lookup(quote, Env, unbound(quote)),
        symbols(X).
    
    evil(Expr, Env, Val) :-
        symbol(Expr),
        lookup(Expr, Env, Val),
        dif(Val, unbound(quote)).
    
    evil([lambda, [X], Body], Env, closure(X, Body, Env)).
    
    evil([list|Tail], Env, Val) :-
        evil_list(Tail, Env, Val).
    
    evil([E1, E2], Env, Val) :- 
        evil(E1, Env, closure(X, Body, Env1_Old)),
        evil(E2, Env, Arg), 
        evil(Body, [X-Arg|Env1_Old], Val).
    
    evil([cons, E1, E2], Env, Val) :-
        evil(E1, Env, E1E),
        evil(E2, Env, E2E),
        Val = [E1E | E2E].
    
    evil_list([], _, []).
    evil_list([H|T], Env, [H2|T2]) :-
        evil(H, Env, H2), evil_list(T, Env, T2).
    
    % evaluate in the empty environment
    
    evil(Expr, Val) :-
        evil(Expr, [quote-unbound(quote)], Val).
    

    Tests:

    Find Scheme expressions that eval to (i love you) -- this example has a history in miniKanren:

    ?- evil(X, [i, love, you]), print(X).
    [quote,[i,love,you]]
    X = [quote, [i, love, you]] ;
    [list,[quote,i],[quote,love],[quote,you]]
    X = [list, [quote, i], [quote, love], [quote, you]] ;
    [list,[quote,i],[quote,love],[[lambda,[_3302],[quote,you]],[quote,_3198]]]
    X = [list, [quote, i], [quote, love], [[lambda, [_3722], [quote|...]], [quote, _3758]]],
    dif(_3722, quote),
    freeze(_3758, atom(_3758)) ;
    [list,[quote,i],[quote,love],[[lambda,[_3234],_3234],[quote,you]]]
    X = [list, [quote, i], [quote, love], [[lambda, [_3572], _3572], [quote, you]]],
    freeze(_3572, atom(_3572)) ;
    

    In other words, the first 4 things it finds are:

    (quote (i love you))
    
    (list (quote i) (quote love) (quote you))
    
    (list (quote i) (quote love) ((lambda (_A) (quote you)) (quote _B)))
    ; as long as _A != quote
    
    (list (quote i) (quote love) ((lambda (_A) _A) (quote you))) 
    ; as long as _A is a symbol
    

    It looks like the Scheme semantics are correct. The language-lawyer type of constraints it places are pretty neat. Indeed, real Scheme will refuse

    > (list (quote i) (quote love) ((lambda (quote) (quote you)) (quote _B)))
    Exception: variable you is not bound
    Type (debug) to enter the debugger.
    

    but will accept

    > (list (quote i) (quote love) ((lambda (quote) quote) (quote you)))
    (i love you)
    

    So how about quines?

    ?- evil(X, X).
    <loops>
    

    miniKanren uses BFS, so maybe that's why it produces results here. With DFS, this could work (assuming there are no bugs):

    ?- call_with_depth_limit(evil(X, X), n, R).
    

    or

    ?- call_with_inference_limit(evil(X, X), m, R).
    

    but SWI doesn't necessarily limit the recursion with call_with_depth_limit.

    0 讨论(0)
  • 2021-01-21 06:34

    One might ask whether an occurs check flag is superior to an explicit unify_with_occurs_check/2. In the solution with explicit unify_with_occurs_check/2 we have placed one such call in lookup2/3 and in quine/3. If we use an occurs check flag, we do not need to manually place such calls and can rely on the dynamics of the Prolog interpreter.

    We removed the explicit unify_with_occurs_check/2 in lookup2/3:

    lookup2(S, [S-T|_], T).
    lookup2(S, [T-_|E], R) :-
       dif(S, T),
       lookup(S, E, R).
    

    And also in quine/3, making it less generate and test, and more constraint logic like. Using the same variable Q twice acts like a constraint that is pushed into the execution:

    quine(Q, M, N) :-
       expr(Q),
       between(0, M, N),
       eval(Q, [], Q, [N], _).
    

    Here are some results for the new SWI-Prolog 8.3.17, which got its unify_with_occurs_check/2 fixed to gether with zhe occurs check flag fixed:

    /* explicit unify_with_occurs_check/2 */
    ?- time((dif(Q, []), quine(Q, 6, N))).
    % 208,612,270 inferences, 11.344 CPU in 11.332 seconds (100% CPU, 18390062 Lips)
    
    /* occurs_check=true */
    ?- time((dif(Q, []), quine(Q, 6, N))).
    % 48,502,916 inferences, 2.859 CPU in 2.859 seconds (100% CPU, 16962768 Lips)
    

    And also a preview for the upcoming Jekejeke Prolog 1.4.7, which will also feature an occurs check flag:

    /* explicit unify_with_occurs_check/2 */
    ?- time((dif(Q, []), quine(Q, 6, N))).
    % Up 37,988 ms, GC 334 ms, Threads 37,625 ms (Current 01/10/21 01:29:35)
    
    /* occurs_check=true */
    ?- time((dif(Q, []), quine(Q, 6, N))).
    % Up 13,367 ms, GC 99 ms, Threads 13,235 ms (Current 01/10/21 01:35:24)
    

    Quite amazing that the occurs check flag can lead to a 3-fold spead-up in both Prolog systems! The result is possibly indicative the way we explicitly placed unify_with_occurs_check/2 was faulty?

    BTW: Open source:

    Quine Generation via Relational Interpreters
    explicit unify_with_occurs_check/2
    https://gist.github.com/jburse/a05e04191dcc68e542567542a7183d3b#file-quine-pl

    Quine Generation via Relational Interpreters
    occurs_check=true
    https://gist.github.com/jburse/a05e04191dcc68e542567542a7183d3b#file-quine2-pl

    0 讨论(0)
提交回复
热议问题