9
votes

Comprendre les listes Prolog

J'essaie de comprendre les listes Prolog et comment les valeurs sont «retournées» / instanciées à la fin d'une fonction récursive.

Je regarde cet exemple simple:

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).


6 commentaires

Alors, comment les X s'uniraient-ils avec R ? Et plus important encore, comment R obtient-il une valeur pour le résultat?


@GuyCoder Veuillez voir la question mise à jour (essentiellement la question que vous m'avez posée, je ne la comprends pas).


L'exemple de prédicat que vous avez donné dans la question que je ne considère pas comme un bon candidat pour en savoir plus sur l'unification et la liste. Un meilleur exemple serait append / 3 .


Ma question vous demande fondamentalement, comment Prolog met-il une valeur dans la variable R à renvoyer? Ce n'est pas le cas. La réponse est renvoyée en troisième position qui est Xs . Lorsque Xs est unifié avec une valeur, alors la troisième positiong étant Xs contiendra le résultat. Quand cela est ensuite unifié avec le prédicat appelant, la troisième position qui est non liée s'unifiera avec la valeur de Xs . Cette réponse pourrait peut-être vous aider.


Comprenez-vous l'unification? Comprenez-vous également le chaînage arrière? Sinon, vous devez d'abord les comprendre. Si quelqu'un publie ces questions sous forme de questions sur StackOveflow et que vous ne pouvez pas y répondre avec confiance, vous devez les étudier davantage.


@GuyCoder Je comprends l'unification et je comprends votre question précédente, mais ce que je ne comprends pas, c'est en quoi le résultat de R est correct, car si vous regardez mon exécution d'un appel de programme, la valeur de Xs n'est pas [1,3] , ce qu'il produit finalement; c'est à la place [3] qui unifie en R (il me manque clairement quelque chose en cours de route, mais je ne suis pas sûr de ce que c'est).


4 Réponses :


2
votes

Du commentaire:

Comment le résultat de R est-il correct, parce que si vous regardez ma course d'un appel de programme, la valeur de Xs n'est pas [1,3], c'est ce qu'elle finalement les sorties; c'est plutôt [3] qui unifie à R (clairement je suis il manque quelque chose en cours de route, mais je ne sais pas ce que c'est).

Ceci est correct:

val_and_remainder(X,[Y|Ys],[Y|R])

cependant Prolog n'est pas comme les autres langages de programmation où vous entrez avec une entrée et sortez avec une sortie à une instruction de retour. Dans Prolog, vous avancez dans les instructions de prédicat en unifiant et en continuant avec des prédicats qui sont vrais, et lors du retour en arrière, unifiez également les variables indépendantes. (Ce n'est pas techniquement correct mais c'est plus facile à comprendre pour certains si vous y pensez de cette façon.)

Vous n'avez pas pris en compte les variables indépendantes qui sont maintenant liées au retour en arrière. P >

Lorsque vous atteignez le cas de base Xs était lié à [3 ,

mais lorsque vous revenez en arrière, vous regardez p >

val_and_remainder(2, [1,2,3], R).

et en particulier [1 | R] pour le troisième paramètre.

Puisque Xs a été unifié avec R dans l'appel au cas de base, c'est-à-dire que

val_and_remainder(2, [1|[2,3]], [1|R])

R a maintenant [3] .

Maintenant, la troisième position du paramètre dans

val_and_remainder(X,[X|Xs],Xs).

est [1 | R] qui est [1 | [3]] qui en tant que sucre syntaxique est [1,3] et pas seulement [3 .

Maintenant lors de l'exécution de la requête

val_and_remainder(2, [1|[2,3]], [1|R])

, le troisième paramètre de la requête R était unifié avec le troisième paramètre du prédicat

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).

donc R a été unifié avec [Y | R] whi ch unpon backtracking est [1,3] et donc la valeur liée à la variable de requête R est [1,3edral


0 commentaires

6
votes

Dans Prolog, vous devez considérer les prédicats pas comme des fonctions comme vous le feriez normalement dans d’autres langues. Les prédicats décrivent des relations qui peuvent inclure des arguments qui aident à définir cette relation.

Par exemple, prenons ce cas simple:

val_and_remainder(X, [Y|Ys], [Y|R]) :- 
    val_and_remainder(X, Ys, R).

Ceci est un prédicat qui définit une relation entre deux arguments. Grâce à l ' unification , cela signifie que le premier et le deuxième arguments sont les mêmes s'ils sont unifiés (et cette définition est à nous, les auteurs du prédicat). Ainsi, same_term (a, a) réussira, same_term (a, b) échouera et same_term (a, X) réussira avec X = a .

Vous pouvez également écrire ceci sous une forme plus explicite:

val_and_remainder(X, [H|T], R) :-
    X = H,
    R = T.

Regardons maintenant votre exemple, val_and_remainder / 3 . Tout d'abord, qu'est-ce que cela signifie?

val_and_remainder(X,[X|Xs],Xs).

Cela signifie que X est un élément de List et Rest est une liste composée de tous les autres éléments (sans X ). (REMARQUE: vous n'avez pas expliqué cette signification tout de suite, mais je détermine cette signification à partir de l'implémentation de votre exemple.)

Nous pouvons maintenant écrire pour décrire les règles. Tout d'abord, un cas de base simple:

val_and_remainder(X, List, Rest)

Ceci dit que:

Xs est le reste de la liste [X | Xs] sans X .

Cette déclaration devrait être assez évidente par la définition de la syntaxe [X | Xs] pour une liste dans Prolog. Vous avez besoin de tous ces arguments car le troisième argument Xs doit s'unifier avec la queue (reste) de la liste [X | Xs] , qui est alors aussi Xs (les variables du même nom sont, par définition, unifiées). Comme précédemment, vous pouvez écrire ceci plus en détail comme suit:

same_term(X, Y) :-
    X = Y.  % X and Y are the same if they are unified

Mais la forme courte est en fait plus claire.

Maintenant, la clause récursive dit:

same_term(X, X).

Cela signifie donc:

[Y | R] est le reste de la liste [Y | Ys] sans X si R est le reste de la liste Ys sans l'élément X .

Vous devez réfléchir à cette règle pour vous convaincre qu'elle est logiquement vraie. Le Y est le même dans le deuxième et le troisième arguments car ils font référence au même élément, ils doivent donc s'unifier.

Ces deux clauses de prédicat forment donc deux règles qui couvrent les deux cas . Le premier cas est le cas simple où X est le premier élément de la liste. Le second cas est une définition récursive pour quand X n'est pas le premier élément.

Lorsque vous faites une requête, telle que val_and_remainder (2, [1,2, 3], R). Prolog cherche à voir s'il peut unifier le terme val_and_remainder (2, [1,2,3], R) avec un fait ou la tête de l'une de vos clauses de prédicat. Il échoue dans sa tentative d'unification avec val_and_remainder (X, [X | Xs], Xs) car il aurait besoin d'unifier X avec 2 , ce qui signifie qu'il faudrait unifier [1,2,3] avec [2 | Xs] qui échoue depuis le premier élément de [1,2, 3] vaut 1, mais le premier élément de [2 | Xs] est 2.

Donc Prolog avance et unifie avec succès val_and_remainder (2, [1,2,3], R) avec val_and_remainder (X, [Y | Ys], [Y | R]) en unifiant X avec 2 , Y avec 1, Ys avec [2,3] et R avec [Y | R] (REMARQUE, ceci est important, la variable R dans votre appel n'est PAS la même que la variable R dans la définition du prédicat, nous devrions donc nommer ce R1 pour éviter cette confusion). Nous nommerons votre R R1 et dirons que R1 est unifié avec [Y | R] .

Lorsque le corps de la deuxième clause est exécuté, il appelle val_and_remainder (X, Ys, R). ou, en d'autres termes, val_and_remainder (2, [2, 3], R) . Cela s'unifiera maintenant avec la première clause et vous donnera R = [3] . Lorsque vous détendez tout cela, vous obtenez, R1 = [Y | [3]] , et en vous rappelant que Y était lié à 1, le résultat est R1 = [1,3] .


1 commentaires

Cela a beaucoup de sens, merci! Désolé de vous bombarder avec une autre question tout de suite, mais y a-t-il une chance que vous puissiez revoir mon autre question s'il vous plaît? stackoverflow.com/questions/54053834/…



3
votes

La reproduction par étapes du mécanisme de Prolog conduit souvent à plus de confusion qu’elle n’aide. Vous avez probablement des notions comme «renvoyer» signifiant quelque chose de très spécifique - plus approprié aux langages impératifs.

Voici différentes approches que vous pouvez toujours utiliser:

Posez la requête la plus générale

... et laissez Prolog vous expliquer de quoi il s'agit.

| ?- val_and_remainder(x, [x,a,x], Ys).
     Ys = [a,x]
  ;  Ys = [x,a]
  ;  false.

Ainsi, Xs et Ys partagent un préfixe de liste, Xs a ensuite un X , suivi d'un repos commun. Cette requête continuerait à produire des réponses supplémentaires. Parfois, vous voulez voir toutes les réponses, alors vous devez être plus précis. Mais ne soyez pas trop précis:

| ?- val_and_remainder(X, Xs, Ys), val_and_remainder(X, Xs, Ys2), dif(Ys,Ys2).
     Xs = [X,_A,X|_B],
     Ys = [_A,X|_B],
     Ys2 = [X,_A|_B],
     dif([_A,X|_B],[X,_A|_B])
  ;  ...

Nous avons donc ici toutes les réponses possibles pour une liste à quatre éléments. Tous.

Tenez-vous en aux objectifs au sol lorsque vous passez par des inférences spécifiques

Donc, au lieu de val_and_remainder (2, [1,2,3], R). code> (qui a évidemment fait tourner la tête) plutôt considérer val_and_remainder (2, [1,2,3], [1,3]). puis val_and_remainder (2, [2,3], [3]) . De ce côté, cela devrait être évident.

Lire les règles Prolog de droite à gauche

Voir les règles Prolog comme règles de production. Ainsi, chaque fois que tout tient du côté droit d'une règle, vous pouvez conclure ce qui est sur la gauche. Ainsi, le : - est une représentation du début des années 1970 d'un ←

Plus tard, vous voudrez peut-être aussi réfléchir à des questions plus complexes. Comme

Dépendances fonctionnelles

Le premier et le deuxième argument déterminent-ils de manière unique le dernier? Est-ce que X , Xs Ys est valable?

Voici un exemple de requête qui demande que Ys et Ys2 soient différents pour les mêmes X et Xs .

| ?- Xs = [_,_,_,_], val_and_remainder(X, Xs, Ys).
     Xs = [X,_A,_B,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,X,_B,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,_B,X,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,_B,_C,X],
     Ys = [_A,_B,_C]
  ;  false.

Donc, apparemment, il y a des valeurs différentes pour Ys pour un X et un Xs . Voici un exemple concret:

| ?- val_and_remainder(X, Xs, Ys).
     Xs = [X|Ys]
  ;  Xs = [_A,X|_B],
     Ys = [_A|_B]
  ;  Xs = [_A,_B,X|_C],
     Ys = [_A,_B|_C]
  ;  Xs = [_A,_B,_C,X|_D],
     Ys = [_A,_B,_C|_D]
  ;  Xs = [_A,_B,_C,_D,X|_E],
     Ys = [_A,_B,_C,_D|_E]
  ; ...

Il n'y a pas de retour classique ici. Il ne revient pas une mais deux fois. C'est plus un yield .

Pourtant, il y a en fait une dépendance fonctionnelle entre les arguments! Peux-tu le trouver? Et pouvez-vous le prouver du point de vue de Prolog (autant que Prolog peut en faire une preuve)?


2 commentaires

pourriez-vous s'il vous plaît clarifier ce que signifie "une dépendance fonctionnelle entre les arguments existe"? Cela signifie-t-il "formuler une requête qui ne produira toujours qu'une seule réponse" (variante: pas plus d'une réponse)?


@Will: Nous avons déjà défini la relation. Et maintenant nous considérons les propriétés de cette relation. L'un est la notion de dépendance fonctionnelle. Cette notion issue des bases de données relationnelles est facilement étendue aux relations infinies. Et c'est ce que je demande: quels arguments déterminent uniquement quels autres arguments. (En ce qui concerne la preuve: une preuve d'une relation qui n'est pas terminale pour la requête la plus générale peut également être non terminale. Eh bien, nous devons l'accepter)



2
votes

Je ne comprends pas le nom de votre prédicat. C'est de toute façon une distraction. La dénomination non uniforme des variables est également une distraction. Utilisons des noms neutres et courts d'une syllabe pour nous concentrer sur le code lui-même dans sa forme la plus claire:

2 ?- proof(2).
true.

3 ?- proof(3).
true.

4 ?- proof(4).
true.

5 ?- proof(5).
true.

Il s'agit donc du select / 3 . Yay !..

Maintenant vous posez la question sur la requête foo (2, [1,2,3], R) et comment R obtient sa valeur définie correctement. La principale chose qui manque dans votre récapitulatif est le changement de nom des variables lorsqu'une clause correspondante est sélectionnée. La résolution de la requête est la suivante:

ground_list(LEN,L):-
  findall(N, between(1,LEN,N), NS), 
  member(N,NS),
  length(L,N), 
  maplist( \A^member(A,NS), L).

bcs(N, BCS):- 
  bagof(B-C, A^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), BCS).

as(N, AS):- 
  bagof(A, B^C^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), AS).

proof(N):-
  as(N,AS), bcs(N,BCS), 
  length(AS,N1), length(BCS, N2), N1 =:= N2.

Les objectifs entre | - et ? sont la résolvante , les équations à l'intérieur de {} sont la substitution . La base de connaissances (KB) est implicitement à gauche de | - dans son intégralité.

A chaque étape, le but le plus à gauche dans la résolvante est choisi, une clause avec la tête correspondante est choisie parmi celles de la KB (en renommant strong> toutes les variables de la clause de manière cohérente, de sorte qu'aucune variable dans la résolution ne soit utilisée par la clause renommée, donc il n'y a pas de capture de variable accidentelle) , et l'objectif choisi est remplacé dans le résolvant avec le corps de cette clause, tandis que l'unification réussie est ajoutée à la substitution. Lorsque la résolvante est vide, la requête a été prouvée et ce que nous voyons est celle qui réussit et -branch dans l'ensemble et-ou arbre .


Voici comment une machine pourrait faire il. Les étapes de "réécriture" sont introduites ici pour faciliter la compréhension humaine.

Nous pouvons donc voir ici que la première sélection de clause réussie aboutit à l'équation

28 ?- length(BB,NN), foo(AA,BB,CC), XX=[AA,BB,CC], numbervars(XX), 
      writeln(XX), (NN>3, !, fail).
[A,[A],[]]
[A,[A,B],[B]]
[A,[B,A],[B]]
[A,[A,B,C],[B,C]]
[A,[B,A,C],[B,C]]
[A,[B,C,A],[B,C]]
[A,[A,B,C,D],[B,C,D]]
false.

29 ?- length(BB,NN), foo(AA,BB,CC), foo(AA2,BB,CC), 
      XX=[AA,AA2,BB,CC],  numbervars(XX), writeln(XX), (NN>3, !, fail).
[A,A,[A],[]]
[A,A,[A,B],[B]]
[A,A,[A,A],[A]]
[A,A,[A,A],[A]]
[A,A,[B,A],[B]]
[A,A,[A,B,C],[B,C]]
[A,A,[A,A,B],[A,B]]
[A,A,[A,A,A],[A,A]]
[A,A,[A,A,B],[A,B]]
[A,A,[B,A,C],[B,C]]
[A,A,[B,A,A],[B,A]]
[A,A,[A,A,A],[A,A]]
[A,A,[B,A,A],[B,A]]
[A,A,[B,C,A],[B,C]]
[A,A,[A,B,C,D],[B,C,D]]
false.

, et le second, -

10 ?- dif(A1,A2), foo(A1,B,C), foo(A2,B,C).
Action (h for help) ? abort
% Execution Aborted

, qui ensemble impliquent

2 ?- foo( A, [0,1,2,1,3], [0,2,1,3]).
A = 1 ;
false.

3 ?- foo( A, [0,1,2,1,3], [0,1,2,3]).
A = 1 ;
false.

4 ?- foo( A, [0,1,2,1,3], [0,1,2,4]).
false.

f ?- foo( A, [0,1,1], [0,1]).
A = 1 ;
A = 1 ;
false.

Cette instanciation / optimisation progressive descendante -en dehors des listes est une manière très caractéristique de Prolog de faire les choses.


En réponse au défi de la prime, concernant la dépendance fonctionnelle dans la relation foo / 3 (ie select / 3 ): dans foo (A, B, C) , deux valeurs au sol quelconques pour B et C déterminent de manière unique la valeur de A ( ou son absence):

     R = [1,        3]

Tentative de réfuter par un contre-argument:

              R1 = [3]

Le prologue ne parvient pas à trouver un contre-argument.

Liens pour voir de plus près ce qui se passe, avec un approfondissement itératif:

     R = [1 | R1     ]

AA et AA2 code> sont toujours instanciés sur la même variable.

Il n'y a rien de spécial à propos du nombre 3, il est donc prudent de conjecturer par généralisation qu'il en sera toujours ainsi, quelle que soit la longueur essayée.

Une autre tentative de preuve Prolog -wise:

|-  foo( 2, [1,2,3], R) ? { } 
  %% SELECT -- 1st clause, with rename
  |-  ?  { foo( H1, [H1|T1], T1) = foo( 2, [1,2,3], R) }
         **FAIL** (2 = 1)
         **BACKTRACK to the last SELECT**
  %% SELECT -- 2nd clause, with rename
  |-  foo( X1, T1, R1) ?
         { foo( X1, [H1|T1], [H1|R1]) = foo( 2, [1,2,3], R) }
         **OK**
    %% REWRITE
    |-  foo( X1, T1, R1) ?
          { X1=2, [H1|T1]=[1,2,3], [H1|R1]=R }
    %% REWRITE
    |-  foo( 2, [2,3], R1) ?  { R=[1|R1] } 
      %% SELECT -- 1st clause, with rename
      |-  ? { foo( H2, [H2|T2], T2) = foo( 2, [2,3], R1), R=[1|R1] }
             ** OK ** 
        %% REWRITE
        |-  ? { H2=2, T2=[3], T2=R1, R=[1|R1] }
        %% REWRITE
        |-  ? { R=[1,3] }
        %% DONE

Ceci compare le nombre de combinaisons BC réussies dans l'ensemble avec le nombre de Un s qu'ils produisent. L'égalité signifie une correspondance un à un.

Et donc nous avons,

foo( H, [H | T], T).                          % 1st clause

foo( X, [H | T], [H | R]) :- foo( X, T, R).   % 2nd clause

Et donc pour tout N qu'il contient . De plus en plus lent. Une requête générale et illimitée est simple à écrire, mais le ralentissement semble exponentiel.


9 commentaires

Je ne sais pas pourquoi vous produisez une autre définition avec un autre nom. La question portait sur la définition du PO.


Vous donnez des exemples concrets, vous pourriez faire mieux que la preuve par un exemple (unique, au sol)


Votre programme est capable de produire un contre-exemple, s'il y en a un! (Après une période indéterminée, en effet). Mais vous devez faire valoir pourquoi il est capable de le faire! L'examen de quelques exemples n'apporte pas ce genre de certitude.


numbervars n'est pas sûr! Pensez à freeze (X, X = 1), numbervars (X, 0, _) - SWI produit une erreur qui est plutôt sympa. Mieux encore, utiliser un meilleur niveau supérieur


merci, j'y travaillerai un peu plus .... (les numbervars ne sont utilisés que pour afficher les résultats de manière lisible). si vous voulez dire SICStus, ce n'est pas gratuit, bien que relativement bon marché pour un usage personnel. sinon, que recommanderiez-vous?


Pour moi, SICStus est le seul choix pour le moment. Mais SWI pourrait aussi s'améliorer. Vous auriez à faire un effort là-bas, cependant


@false merci beaucoup pour la prime. :) Je suppose que vous vouliez aussi un peu plus que ça. :) Je ne comprends pas exactement comment faire travailler bagof et ses amis ici quand il y a des variables non corroborées, donc j'ai dû faire des tests approfondis à la place. aurait peut-être dû utiliser la gamme 2N pour que la deuxième liste soit vraiment exhaustive ...


Comment votre réponse peut-elle être utile à d'autres lecteurs? Tout d'abord, ce changement de nom. Pourquoi faites-vous cela du tout. C'est ce petit effort supplémentaire qui pousse les gens à arrêter de lire ...


comme je l'ai dit dans la réponse, je ne pouvais pas comprendre ce que signifiait "val_and_remainder". donc pour ne pas me laisser distraire, je lui ai donné un nom neutre, pour me concentrer uniquement sur le code. même chose avec les noms de variables. Pour moi, c'était économiser l'effort, et devoir répéter le nom était plus difficile, pour moi. si c'est sans signification, mieux si c'est court. c'est une question de compréhension / perception personnelle. comme on dit, "YMMV."