Search code examples
prolog

Collecting entity-value pairs - a canonical solution?


I have the following predicate which transforms entity-attribute-value facts to dicts. I'd be interested if there is a better way to express this as it seems excessively verbose for what it achieves.

It makes use of dict_pairs (which I think is SWI prolog only), but the real work is in build a list KVPairs (key-value pairs).

I there a more canonically Prolog way of writing this?

% Database
% eav(EntiyId, Attribute, Value)
eav(1,attr1,123).
eav(1,attr2,456).
eav(2,attr1,0).
eav(2,attr1,123).            

% Match P(Id, A, Val)
% to Tag{A:V, ... } for each A in Attrs
relation_dict(P, Id, Attrs, Tag, Dict) :-
    relation_attr_val_(P, Id, Attrs, KVPairs) ,
    dict_pairs(Dict, Tag, KVPairs)
    . 
relation_attr_val_(_, _, [], Pairs) :-
    Pairs = [].

relation_attr_val_(P, Id, [Attr|Attrs], Pairs) :-
    G=..[P, Id, Attr, Val],
    G,
    relation_attr_val_(P, Id, Attrs, Pairs__),
    append([(Attr-Val)], Pairs__, Pairs),
    !.

% No match for P(Id, Attr, _) -- try next
relation_attr_val_(P, Id, [_|Attrs], Pairs) :-
    relation_attr_val_(P, Id, Attrs, Pairs).
    .                                        
?- relation_dict(eav, 1, [attr1, attr2], tag, Result).
tag{attr1:123, attr2:456}.

NB. I'd wanted to avoid iterating over all eav(Id, Attr, _) where Attr may not be in the list specified in the query.

A Pythonic equivalent would be just:

d = {k:db_getval(p,id,attr)
     for k in attrs
      if db_has_val(p,id,attr)}

or for just collecting the KV pairs:

kvs = {(k,db_getval(p,id,attr))
        for k in attrs
        if db_has_val(p,id,attr)}

... which I don't think is particularly dependent on 'imperative-style'. Shouldn't we a expected a similarly compact and readable prolog expression (at least for collecting the KV-pairs)


Solution

  • I would use findall/3 as in:

    relation_dict(P, Id, Attrs, Tag, Dict) :-
        G=..[P, Id, Attr, Val],
        findall(Attr-Val,(
                member(Attr,Attrs),
                call(G)
            ), KVPairs),
        dict_pairs(Dict, Tag, KVPairs).
    

    A findall is used to find all the solution to its second (meta-)argument.

    Here, the first line of the findall/3 specifies what to build (pairs), the second line generates all the members of the list, the third line filters the ones that satisfy the predicate, and the fourth line specifies the variable in which to collect the results.

    I believe this is quite close in intent and compactness to the Python version.