【问题标题】:Read a list of records and perform ongoing calculations about the previous records using Prolog使用 Prolog 读取记录列表并执行有关先前记录的持续计算
【发布时间】:2019-06-21 18:10:38
【问题描述】:

这是一个游乐场示例,灵感来自我曾经做过的一个真实任务(要复杂得多)。基本流程是从顺序文件中读取记录。一些记录包含需要检查先前记录以计算值的命令。

这个解决方案的缺点是它需要一个额外的列表,因此需要额外的重复存储。 该额外列表在以下代码中称为 REMEMBER。 这个例子有一个简单的记录结构,只包含一个数据值,所以复制 REMEMBER 列表中的所有内容并不是一个真正的问题。 但请假设实际任务涉及更复杂的记录结构,因此复制 REMEMBER 列表中的所有内容是非常不可取的。

我倾向于使用双向链表,但是根据此链接的讨论 Doubly Linked List in Prolog 似乎这不是 Prolog 做事的方式。 因此,我很想看看 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
*/

【问题讨论】:

  • 感谢@GuyCoder 的编辑。我应该提到我正在使用 SWI-Prolog 我认为库(dcg/basics)是特定于 SWI-Prolog 的。
  • 有什么原因你不能从头到尾向后处理文件?
  • 不允许使用key-value pair 吗?目前我看到您的问题的方式是您需要动态访问记录,而 Prolog 喜欢处理列表,但不需要数据在列表中,因此我得到了您对双向链表的渴望。此外,我从未在 Prolog 中使用过键值对,但如果这个答案需要它,那么我认为没有理由避免它们。
  • 感兴趣的:dicts - 从未使用过这个,但有一个 dicts_slice/3 可能有用。
  • 如果您的文件(比如说)有 10,000 行长,有什么东西可以阻止最后一行成为“平均 10000”并迫使您重新访问到目前为止所看到的所有数据吗?如果没有,我认为地球上没有一种力量可以使您不必在整个处理过程中保留文件的全部内容。

标签: list prolog


【解决方案1】:

遵循 Logtalk + SWI-Prolog 解决方案,不需要任何双链表的具体化。只需要一个堆栈,使用列表简单地实现:

------------ 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.
-------------------------------------

使用问题中的数据文件的示例调用:

?- {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)] .

正如您所注意到的,我对报告使用了比列表更合理的表示形式。通过避免长度为 Count 的堆栈前缀被遍历两次并创建临时值列表,可以通过将 take/3sum/2 调用组合到自定义谓词中来编写更有效的解决方案。例如:

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).

从示例数据文件来看,该文件似乎可以以计算文件中所有值的平均值的请求结束。因此,report//2 非终端必须保留整个堆栈,直到处理完所有数据文件。

【讨论】:

  • @GuyCoder 注意:- uses(list, [take/3]). 指令。解决这个问题不需要约束。
  • @GuyCoder listnumberlist 是 Logtalk 库对象。您可以在logtalk.org/library/index.html 查阅他们的文档
  • 感兴趣的:Logtalk take/3
  • 非常好的代码。但我注意到它并没有解决 OP 中的愿望,即避免堆栈REMEMBER(在您的解决方案中仅称为Stack),而且可能会加剧问题,因为使用另一个临时列表Values 为@987654336 @ 和 sum 。此外,一个批评 -> 使用晦涩的名称 'r' 而不是 'record' 的错误。不知何故,这个例子在我脑海中凝聚了这个想法。看来我应该重新访问 logtalk 。不要获得基于实例的 getter/setter 功能,这是我的拐杖。而是因为 logtalk(也许主要是)是组织代码的好方法?
  • @S.Selfial 请参阅我的答案的结尾注释,其中涉及堆栈和临时列表的使用。 “基于实例的 getter/setter 能力”也是一种命令模式; Logtalk 是声明性的
【解决方案2】:

我会尝试利用 DCG 半上下文,正如 on metalevel.at page 所解释的那样。 一个我认为很容易掌握的 OT 示例是 this answer(在 DCG 中解决斑马状谜题)。

hint1 -->
  kind(brad, K), {dif(K, wheat)}, topping(brad, plain), size(walt, small).
hint2 -->
  size(P1, medium), size(P2, medium), {P1 \= P2},
  flavor(P1, hazelnut), topping(P2, peanut_butter).
...

提示“通过魔法”访问上下文共享:

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

必须以这种方式调用 DCG,并提供相关的初始状态:

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

现在,对于您的应用案例,这几乎是无用的(您已经解决了,只是以一种冗长的方式)。首先,我不明白你为什么要执行 2 遍算法。考虑一下代码可以多么简洁,它产生与您发布的结果完全相同的结果(只是显示不同),一次通过,利用 library(aggregate) 执行算术。顺便说一句,为什么 clpfd、clpr 也可以算……您真的对 CLP 的服务感兴趣吗?

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).

产量:

?- 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 .

无论如何,swipl addons 提供一些有用的材料。参见例如edgc,一个处理许多多个累加器的扩展,同时对Acquarius Prolog compiler的具体语法树进行多次访问——发展到90年代。

【讨论】:

  • 感谢 edgc 的链接。我经常发现我的 DCG 需要多个线程累加器(隐式状态)。
【解决方案3】:

因此,对于初学者,我注意到您可以在任一方向构建结果。换句话说,经典的“int list”语法是这样的:

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

这样工作:

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

但是你可以翻转它,让它像这样向后解析列表:

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

这行得通,ish:

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

这样做的问题是,将递归调用放在前面,然后是可以匹配空列表的“空白”之类的东西是堆栈爆炸的秘诀。但是,您也可以通过 DCG 本身传递“先前”状态来向后解析事物:

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] .

现在,我认为我们可以很好地解决您的问题;我写了我的解决方案,现在看到它与上面的@PauloMoura 非常相似,但无论如何都是这样:

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.

这似乎给你的例子类似的输出:

?- 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)]

现在,将它扩展到双向链表,让我们首先看看我们需要做什么来以双向链接的方式处理“int list”语法。就像这个一样,我们必须将前一个链接转发到递归调用中,但是比这个更糟糕的是,我们需要用当前节点填充我们收到的前一个变量中的“下一个”链接。但是因为那个链接第一次是nil,我们必须有一些条件逻辑来忽略那个。而且我想不出一个合理的空双向链表,所以我将基本情况更改为[X] 而不是[]。所以它变得有点恶心。

% 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).

注意Cur = node(..., node(..., Cur), ...) 中的自引用。这种统一就是前一个链接和这个链接之间的“结”。让我们试试吧:

?- 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) 

有点难读,但基本上,从 9 点到 45 点到 23 点到 1。我们从后到前解析它,并用两个方向的指针结束。

此时剩下要做的是将解析器更改为使用这些指针发出记录,并编写一个以这种方式工作的平均器。我无法在原地进行平均,所以我写了一个帮助程序,从一个双向链表中给我“最多 N 个”:

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).

这样工作:

?- 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] .

有了这个实用程序,我们就可以完成这个了:

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.

总的来说,我很高兴我能够完成这项工作,但在这个公式中,你真的不需要双向链表中的“下一个”指针,所以我倾向于只使用列表上面的实现(或者如果我在看 Logtalk,可能是 Paulo 的实现)。希望这说明了如何使用双向链表执行此操作,如果您的实际问题需要它,尽管您的模型并不真正需要它。希望对您有所帮助!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-16
    • 2016-05-27
    相关资源
    最近更新 更多