3
votes

Lire une liste d'enregistrements et effectuer des calculs en cours sur les enregistrements précédents à l'aide de Prolog

Voici un exemple de terrain de jeu inspiré d'une tâche réelle (beaucoup plus compliquée) que j'ai faite autrefois. Le flux de base est qu'il y a la lecture d'enregistrements à partir d'un fichier séquentiel. Certains des enregistrements contiennent des commandes qui nécessitent l'examen des enregistrements précédents pour calculer une valeur.

Ce n'est pas souhaitable avec cette solution, c'est qu'elle nécessite une liste supplémentaire donc un stockage supplémentaire en double. Cette liste supplémentaire est appelée REMEMBER dans le code suivant. Cet exemple a une structure d'enregistrement simple ne contenant qu'une seule valeur de données, donc dupliquer tout dans la liste REMEMBER n'est pas un réel problème. Mais s'il vous plaît, supposez que la vraie tâche implique une structure d'enregistrement beaucoup plus compliquée de sorte que la duplication de tout dans la liste REMEMBER est très indésirable.

Je suis cependant enclin à utiliser une liste à double chaînage par discussion sur ce lien Liste doublement liée dans Prolog il semble que ce ne soit pas la manière Prolog de faire les choses. Par conséquent, je suis intéressé de voir ce que devrait être la manière de faire Prolog.

/*
A file contains sequential records.
There are two types of record.
A data record provides a data value.
An average record provides a count and is a request for an average of the last count data values.
The parse dcg below parses a list from the data file.
The report dcg uses that list to generate a report.

After parse the list looks like this:

[value=5.9,value=4.7,value=7.5,average=3,value=9.0,value=1.1,value=8.3,average=5,value=7.1,value=1.3,value=6.7,value=9.9,value=0.5,value=0.3,value=1.5,value=0.2,average=7,value=2.2,value=7.8,value=2.5,value=4.5,value=2.4,value=9.7,average=4,value=5.2,value=8.5,value=2.2,value=8.0,value=0.7].

An example report looks like this:

[[count=3,total=18.1,average=6.033333333333333],[count=5,total=30.599999999999998,average=6.12],[count=7,total=20.400000000000002,average=2.9142857142857146],[count=4,total=19.1,average=4.775]].
*/

:- use_module(library(dcg/basics)).
:- use_module(library(readutil)).
:- use_module(library(clpfd)).
:- use_module(library(clpr)).

dospy
:-
spy(report),
spy(average),
leash(-all).

:- initialization main.

report(LIST)
-->
% the report starts with nothing to REMEMBER.
report(LIST,[]).

report([value=VALUE|LIST],REMEMBER)
-->
% value in the LIST goes into REMEMBER.
report(LIST,[value=VALUE|REMEMBER]).

report([average=COUNT|LIST],REMEMBER)
-->
% request for average in the LIST.
average(REMEMBER,COUNT),
report(LIST,REMEMBER).

report([],_REMEMBER)
% the LIST is empty so the report is done.
--> [].

average(REMEMBER,COUNT)
-->
% the average starts at 0 then accumulates for COUNT values from REMEMBER.
average(REMEMBER,COUNT,0,0.0).

average([value=VALUE|REMEMBER],COUNT,AT,TOTAL)
-->
% found needed value in the REMEMBER.
clpfd( AT #\= COUNT ),
clpfd( AT_NEXT #= AT + 1 ),
clpr( TOTAL_NEXT = TOTAL + VALUE ),
average(REMEMBER,COUNT,AT_NEXT,TOTAL_NEXT).

average(_REMEMBER,COUNT,COUNT,TOTAL)
-->
% all of the needed value have been seen so calculate and add to report. 
clpr( AVERAGE = TOTAL / COUNT ),
[[count=COUNT,total=TOTAL,average=AVERAGE]].

% now the part that does the parse of the data file.

parse(LIST) --> parse(data,LIST).
parse(LIST) --> parse(average,LIST).
parse(LIST) --> parse(end,LIST).

parse(data,[value=FLOAT|LIST])
-->
"data", whites, float(FLOAT), blanks, !,
parse(LIST).

parse(average,[average=COUNT|LIST])
-->
"average", whites, integer(COUNT), blanks, !,
parse(LIST).

parse(end,[])
-->
[].

clpr( CLPR )
-->
{ clpr:{ CLPR } }.

clpfd( CLPFD )
-->
{ CLPFD }.

main :-
system:( read_file_to_codes("doubly_motivation_data.txt",CODES,[]) ),
prolog:( phrase(parse(LIST),CODES) ),
system:( writeq(LIST),writeln(.) ),
prolog:( phrase(report(LIST),REPORT) ),
system:( writeq(REPORT),writeln(.) ).

/* doubly_motivation_data.txt
data 5.9
data 4.7
data 7.5
average 3
data 9.0
data 1.1
data 8.3
average 5
data 7.1
data 1.3
data 6.7
data 9.9
data 0.5
data 0.3
data 1.5
data 0.2
average 7
data 2.2
data 7.8
data 2.5
data 4.5
data 2.4
data 9.7
average 4
data 5.2
data 8.5
data 2.2
data 8.0
data 0.7
*/


16 commentaires

Merci pour l'édition @GuyCoder. J'aurais dû mentionner que j'utilise SWI-Prolog Je pense que la bibliothèque (dcg / basics) est spécifique à SWI-Prolog.


Y a-t-il une raison pour laquelle vous ne pouvez pas traiter le fichier à l'envers, de la fin au début?


L'utilisation d'une paire clé-valeur n'est pas autorisée? Actuellement, la façon dont je vois votre problème est que vous devez accéder dynamiquement aux enregistrements et bien que Prolog aime traiter la liste, il n'est pas nécessaire que les données soient dans une liste, donc je comprends votre désir de liste doublement liée. De plus, je n'ai jamais utilisé de paires clé-valeur avec Prolog, mais si cette réponse en a besoin, je ne vois aucune raison de les éviter.


D'intérêt: dicts - Jamais utilisé mais a un dicts_slice / 3 qui pourrait fonctionner.


D'intérêt: listes - Contient nth0 / 3 qui, même s'il n'est pas efficace, devrait fonctionner.


Puisque Paulo a donné une réponse Logtalk qui est très similaire à une réponse SWI-Prolog, je ne la posterai pas. La seule vraie différence que je ferais est d'implémenter le Logtalk take / 3 avec SWI-Prolog nth0 / 3 qui, bien qu'inefficace, devrait fonctionner.


Notez également que vous avez utilisé des contraintes pour faire le calcul, mais en utilisant {} avec DCG, il est possible d'utiliser directement Prolog avec DCG et d'éviter les bibliothèques de contraintes toutes ensemble.


Une variante intéressante qui devrait être beaucoup plus rapide qu'une simple liste serait d'implémenter les données sous forme d'arbre de recherche binaire en utilisant la différence lis. La longueur de la liste devrait être de plusieurs milliers et plus pour vraiment remarquer une différence, car rien de moins serait si rapide dans les deux cas, cela n'aurait pas d'importance.


D'intérêt: Listes de différences - Parle de l'arbre binaire comme liste de différences. Donne quelques photos, mais l'essentiel du travail est laissé sous forme d'exercices.


D'intérêt: Efficacité de la différence -List Programming - Plus d'informations sur les arbres binaires comme liste de différences.


Si votre fichier fait (disons) 10 000 lignes, y a-t-il quelque chose qui empêche la dernière ligne d'être «moyenne de 10 000» et vous oblige à revoir toutes les données que vous avez vues jusqu'à présent? Sinon, je ne pense pas qu'il y ait une force sur terre qui puisse vous éviter d'avoir à conserver tout le contenu du fichier tout au long de son traitement.


@Daniel Lyons: Oui, l'intention était qu'il soit possible pour un enregistrement qui est une "demande moyenne" d'exiger peut-être tous les enregistrements précédents qui sont des "valeurs de données". Le souhait exprimé dans le PO n'était pas d'éviter d'avoir à conserver tous les enregistrements en mémoire. L'intention était d'éviter d'avoir à créer une deuxième structure de données pour que la moyenne puisse être calculée.


L'entrée est-elle un fichier statique, ou pourrait-elle être dynamique comme une entrée utilisateur. Si l'entrée est statique, le fichier peut être filtré en deux fichiers, l'un avec uniquement les données et l'autre avec les emplacements des rapports. Le nouveau fichier deviendrait alors le magasin où l'index correspond à la ligne du fichier. Je ne connais aucune bibliothèque Prolog qui puisse indexer dans un fichier texte et extraire une ligne. En utilisant cela, la taille des données pourrait être beaucoup plus grande que la mémoire disponible.


J'essaie de préparer une solution en utilisant la liste à double lien de l'autre question, mais cela peut me prendre quelques heures de plus (au travail).


Une question inspirante. Bonne utilisation du dcg. Si vous pouvez vous débarrasser du ! , vous êtes sur la voie de la pureté logique.


Aussi, un reproche, ne faites pas par exemple [average = 3.1415] . C'est une utilisation inappropriée du symbole = . = dans Prolog est systématiquement cohérent avec les mathématiques. Ne faites pas non plus ceci [key1-3.1415, key2-0.11235813] (parfois vu dans le code prologue), car une utilisation inappropriée de - . Avec - , les opérandes doivent avoir le même type, vous ne pouvez pas faire cela en maths par exemple 12 pommes - 4 oranges . Vous pouvez utiliser la notation d'ensemble de type json {key1: {3.1415}, key2: {0.11235813}} . Mettez { et } autour des valeurs pour assurer la cohérence avec les mathématiques et Knuth / Tangle.


3 Réponses :


4
votes

Suit une solution Logtalk + SWI-Prolog, qui ne nécessite aucune matérialisation de listes à double lien. Seule une pile, implémentée de manière triviale à l'aide d'une liste, est requise:

compute_record(Count, Stack, r(Count,Total,Average)) :-
    compute_record(0, Count, Stack, 0, Total),
    Average is Total / Count.

compute_record(Count, Count, _, Total, Total) :-
    !.
compute_record(Count0, Count, [Value| Stack], Total0, Total) :-
    Count1 is Count0 + 1,
    Total1 is Total0 + Value,
    compute_record(Count1, Count, Stack, Total1, Total).

Exemple d'appel utilisant le fichier de données dans la question:

?- {library(types_loader), library(reader_loader), reports}.
...

?- reports::process('doubly_motivation_data.txt', Report).
Report = [r(3, 18.1, 6.033333333333334), r(5, 30.599999999999998, 6.119999999999999), r(7, 20.400000000000002, 2.9142857142857146), r(4, 19.1, 4.775)] .

Comme vous remarqué, j'utilise une représentation plus sensée pour le rapport qu'une liste de listes. Une solution un peu plus efficace peut être codée en combinant les appels take / 3 et sum / 2 dans un prédicat personnalisé en évitant le préfixe de pile de longueur Count code> étant parcouru deux fois et créant une liste temporaire de valeurs. Par exemple:

------------ reports.lgt ------------
% load the required modules
:- use_module(library(dcg/basics), []).

% ensure desired interpretation of double-quoted text
:- set_prolog_flag(double_quotes, codes).

% optimize the generated code
:- set_logtalk_flag(optimize, on). 


:- object(reports).

    :- public(process/2).

    :- uses(list, [take/3]).
    :- uses(numberlist, [sum/2]).
    :- uses(reader, [file_to_codes/2]).

    :- use_module(dcg_basics, [blanks//0, whites//0, integer//1, float//1]).

    process(File, Report) :-
        file_to_codes(File, Codes),
        phrase(report(Report), Codes).

    report(Report) -->
        data(Value),
        report(Report, [Value]).

    report([], _) -->
        eos.
    report([Record| Records], Stack) -->
        average(Count),
        {compute_record(Count, Stack, Record)},
        report(Records, Stack).
    report(Records, Stack) -->
        data(Value),
        report(Records, [Value| Stack]).

    average(Count) -->
        "average", whites, integer(Count), blanks.

    data(Value) -->
        "data", whites, float(Value), blanks.

    compute_record(Count, Stack, r(Count,Total,Average)) :-
        take(Count, Stack, Values),
        sum(Values, Total),
        Average is Total / Count.

:- end_object.
-------------------------------------

À partir de l'exemple de fichier de données, il semble que le fichier puisse se terminer par une demande de calcul de la moyenne de toutes les valeurs dans fichier. Ainsi, le report // 2 non-terminal doit conserver toute la pile jusqu'à ce que tout le fichier de données soit traité.


7 commentaires

@GuyCoder Notez la directive : - uses (list, [take / 3]). . Il n'y a pas besoin de contraintes pour résoudre ce problème.


@GuyCoder list et numberlist sont des objets de la bibliothèque Logtalk. Vous pouvez consulter leur documentation sur logtalk.org/library/index.html


D'intérêt: Logtalk take / 3


Très beau code. Mais je note que cela ne répond pas au désir de l'OP, qui était d'éviter la pile REMEMBER (juste appelée Stack dans votre solution), et peut-être exacerbe le problème car en utilisant une autre liste temporaire de Valeurs pour take et sum . Aussi, une critique -> une erreur d'utiliser le nom obscur «r» au lieu de «record». D'une manière ou d'une autre, cet exemple a fusionné cette pensée dans mon esprit. Il semble que je devrais revoir logtalk. Ne pas obtenir la capacité getter / setter basée sur l'instance qui est ma béquille. Mais plutôt parce que logtalk est (peut-être surtout) un excellent moyen d'organiser le code?


@ S.Selfial Voir les notes de fin de ma réponse, qui traitent de l'utilisation de la pile et de la liste temporaire. La "capacité de lecture / définition basée sur une instance" est également un modèle impératif ; Logtalk est déclaratif .


@Paulo a écrit Voir les notes de fin de ma réponse "qui étaient" Une solution un peu plus efficace peut être codée en combinant les appels take / 3 et sum / 2 dans un prédicat personnalisé "... la création d'un prédicat personnalisé ne résoudrait pas le problème , c'est-à-dire que l'approche nécessite la création d'une autre structure de liste temporaire.


@ S.Selfial Mettez à jour ma réponse avec un prédicat alternatif pour calculer un enregistrement individuel qui combine les appels take / 3 et sum / 2 et évite de créer une autre liste temporaire.



2
votes

J'essaierais d'exploiter DCG semicontext, comme expliqué sur la page metalevel.at . Un exemple OT, je pense qu'il est facile à saisir, est cette réponse (résoudre un casse-tête semblable à un zèbre dans un DCG). XXX

Les astuces accèdent au partage de contexte 'par magie':

?- cc_main.
ave(6.033333333333334)=sum(18.1)/3
ave(6.119999999999999)=sum(30.599999999999998)/5
ave(2.9142857142857146)=sum(20.400000000000002)/7
ave(4.775)=sum(19.1)/4
true .

le DCG doit être appelé de cette manière, en fournissant l'état initial pertinent:

cc_main :-
    %system:( read_file_to_codes("doubly_motivation_data.txt",CODES,[]) ),
    codes(CODES),
    tokenize_atom(CODES, Tks),
    phrase(cc_report([],Rep), Tks),
    maplist(writeln, Rep).

cc_report(_,[]) --> [].
cc_report(R,Re) -->
    [data,N],
    cc_report([N|R],Re).
cc_report(R,[ave(Ave)=sum(Sum)/C|Re]) -->
    [average,C],
    {aggregate_all((sum(V),count),(
         % no need for doubly linked lists, just peek from stack...
         nth1(I,R,V),I=<C
       ),(Sum,Count)),Ave is Sum/Count},
    cc_report(R,Re).

Maintenant, pour votre cas applicatif, c'est presque inutile (vous avez déjà résolu, juste de manière verbeuse). Pour commencer, je ne comprends pas pourquoi vous effectuez un algorithme à 2 passes. Considérez à quel point le code pourrait être concis, qui donne les mêmes résultats que ce que vous avez publié (juste affiché différemment), en un seul passage, en utilisant la bibliothèque (agrégat) pour effectuer de l'arithmétique. BTW, pourquoi clpfd, clpr peuvent également compter ... êtes-vous vraiment intéressé par les services de CLP pour une tâche aussi simple?

bagels(Sol):- Sol =
    [[brad,_,_,_,_],
     [walt,_,_,_,_],
    ...],
  phrase((hint1, hint2, hint3, hint4, hint5, hint6), [state(Sol)], _).

donne:

kind(P, K) --> state([P, K, _, _, _]).
topping(P, T) --> state([P, _, T, _, _]).
...

Quoi qu'il en soit, les addons swipl offrent du matériel utile. Voir par exemple edgc , une extension pour gérer beaucoup de plusieurs accumulateurs en effectuant plusieurs visites de l'arbre de syntaxe concret de Acquarius Prolog compilateur - développé dans les années 90.


1 commentaires

Merci pour le lien vers edgc. Je trouve souvent que mes DCG ont besoin de plus d'un accumulateur threadé (état implicite).



1
votes

Donc, pour commencer, j'ai remarqué que vous pouvez générer des résultats dans les deux sens. En d'autres termes, la grammaire classique "liste int" est quelque chose comme ceci:

commandsdl(Report) --> commandsdl(nil, _, Report).
commandsdl(Prev, Prev, []) --> [].
commandsdl(Prev, Last, Report) -->
    record(data(V)),
    blanks,
    {
     Prev = node(PV, PP, Cur)
     ->
         Cur = node(V, node(PV, PP, Cur), _)
     ;
         Cur = node(V, Prev, _)
    },
    commandsdl(Cur, Last, Report).
commandsdl(Prev, Last, [report(Count, Total, Avg)|Report]) -->
    record(average(N)),
    blanks,
    {
       calculate_average_dl(N, Prev, Count, Total, Avg)
    },
    commandsdl(Prev, Last, Report).

calculate_average_dl(N, Prev, Count, Total, Avg) :-
    take_upto(N, Prev, L),
    length(L, Count),
    sumlist(L, Total),
    Avg is Total / Count.

Cela fonctionne comme ceci:

?- phrase(dlist(X), "1 2 3 4 5 6 7 8 9 10"), take_upto(5, X, L).
X = node(10, _S2, nil), % where
   ... [trimmed]
L = [6, 7, 8, 9, 10] .


?- phrase(dlist(X), "1 2 3 4 5 6 7"), take_upto(15, X, L).
X = node(7, _S2, nil), % where
   ... [trimmed]
L = [1, 2, 3, 4, 5, 6, 7] .

Mais vous pouvez retournez-le pour qu'il analyse la liste à l'envers comme ceci:

take_upto(N, DL, Result) :- take_upto(N, 0, DL, [], Result).
take_upto(N, N, _, Result, Result).
take_upto(_, _, nil, Result, Result).
take_upto(N, I, node(V, Prev, _), Rest, Result) :-
    I < N,
    succ(I, I1),
    take_upto(N, I1, Prev, [V|Rest], Result).

Cela fonctionne, ish:

?- phrase(dlist(L), "1 23 45 9").
L = node(9, _S2, nil), % where
    _S1 = node(1, nil, node(23, _S1, _S2)),
    _S2 = node(45, node(23, _S1, _S2), _71658) 

Le problème avec c'est que mettre l'appel récursif au premier plan, suivi de quelque chose comme des "blancs" qui peuvent correspondre à des listes vides est une recette pour une explosion de pile. Mais, vous pouvez également analyser les choses à l'envers en passant l'état "précédent" via le DCG lui-même:

% entry point (nil meaning there is no previous)
dlist(X) --> dlist(nil, X).

% base case: last integer
dlist(Prev, node(X, Prev, nil)) --> integer(X).
dlist(Prev, Last) -->
    integer(X), whites,
    {
     Prev = node(PV, PP, Cur)
     -> 
         Cur = node(X, node(PV, PP, Cur), _)
     ;
         Cur = node(X, Prev, _)
    },
    dlist(Cur, Last).

Maintenant, je pense que nous pouvons résoudre votre problème simplement à partir de cela; J'ai écrit ma solution et je vois maintenant qu'elle est assez similaire à celle de @ PauloMoura ci-dessus, mais la voici quand même:

?- phrase_from_file(commands(C), 'mdata.txt'), write_canonical(C).
[report(3,18.1,6.033333333333334),
 report(5,30.599999999999998,6.119999999999999),
 report(7,20.400000000000002,2.9142857142857146),
 report(4,19.1,4.775)]

Cela semble donner un résultat similaire à votre exemple: p >

commands(Report) --> record(data(V)), blanks, commands([V], _, Report).

commands(Prev, Prev, []) --> [].
commands(Prev, Last, Report) -->
    record(data(V)),
    blanks,
    commands([V|Prev], Last, Report).
commands(Prev, Last, [report(Count, Total, Avg)|Report]) -->
    record(average(N)),
    blanks,
    { calculate_average(N, Prev, Count, Total, Avg) },
    commands(Prev, Last, Report).

calculate_average(N, Prev, Count, Total, Avg) :-
    length(L, N),
    append(L, _, Prev),
    sumlist(L, Total),
    Avg is Total / N,
    Count = N.

Maintenant, en l'étendant à la liste doublement liée, voyons d'abord ce que nous aurions besoin de faire pour gérer la grammaire "int list" de manière doublement liée. Tout comme celui-ci, nous devons transmettre un lien précédent à l'appel récursif, mais le rendant un peu pire que celui-ci, nous devons remplir le lien "suivant" dans la variable précédente que nous recevons, avec le nœud actuel. Mais comme ce lien sera nul la première fois, nous devons avoir un peu de logique conditionnelle pour ignorer celui-là. Et je ne pouvais pas penser à une liste doublement liée vide sensée, alors j'ai changé le cas de base pour être [X] au lieu de [] . Donc ça devient un peu sale.

rintlist(L) --> rintlist([], L).
rintlist(Prev, Prev) --> [].
rintlist(Prev, Last) --> integer(X), whites, rintlist([X|Prev], Last).

?- phrase(rintlist(X), "1 23 45 9").
X = [9, 45, 23, 1] .

Notez l'auto-référence dans Cur = node (..., node (..., Cur), .. .) . Cette unification est en quelque sorte ce qui "fait le nœud" entre le lien précédent et ce lien. Essayons-le:

?- phrase(rintlist(X), "1 23 45 9").
X = [9, 45, 23, 1] 

Un peu difficile à lire, mais en gros, 9 points à 45 points à 23 points à 1. Nous l'avons analysé dos à front et avons terminé avec des pointeurs dans les deux sens.

Ce qui reste à faire à ce stade est de changer l'analyseur pour émettre des enregistrements avec ces pointeurs à la place, et d'écrire un moyenneur qui fonctionne de cette façon. Je ne pouvais pas vraiment y arriver en faisant la moyenne sur place, alors j'ai écrit une aide pour me donner "jusqu'à N précédents" à partir d'une liste à double lien:

rintlist([]) --> [].
rintlist([X|Xs]) --> rintlist(Xs), whites, integer(X).

Cela fonctionne comme ceci:

?- phrase(intlist(X), "1 23 45 9").
X = [1, 23, 45, 9] ;

Avec cet utilitaire en place, nous pouvons terminer ceci:

intlist([]) --> [].
intlist([X|Xs]) --> integer(X), whites, intlist(Xs).

Dans l'ensemble, je Je suis ravi d'avoir pu faire ce travail, mais dans cette formulation, vous n'avez vraiment pas besoin des pointeurs "suivant" dans votre liste à double lien, donc je serais enclin à simplement opter pour l'implémentation de la liste ci-dessus mise en œuvre si je regardais Logtalk). J'espère que cela illustre comment vous pouvez faire cela avec des listes à double lien, si votre problème réel l'exige alors que votre modèle n'en a pas vraiment besoin. J'espère que ça aide!


0 commentaires