No query result with dict and javascript object which has _ values

Hey all,

I am using swipl-wasm@^5.0.14: and I have some trouble finding a way how to ignore certain dict values when I query from javascript and using the options parameter. I am also new in prolog so it is possible the issue is with the query itself.

In the code below there is a simple fact which contains a dict and an atom. I want to get all atoms by passing a dict with values set to _. I need this as in the real code those fields will be set one by one.

Given the following prolog code:

predicate_with_dict(_{a:b,b:c}, good).
predicate_with_dict(_{a:d,b:e}, very_good).

If I run this javascript code:

  for (const [query, option] of [
    ["predicate_with_dict(_{a:d,b:e}, X)",{}],
    ["predicate_with_dict(Input, X)", {Input:{a:"d",b:"e"}}],
    ["predicate_with_dict(_{a:_,b:_}, X)", {}],
    ["predicate_with_dict(Input, X)", {Input:{a:"_",b:"_"}}],
  ]) {
    var r = swipl.prolog.query(query, option).once()
    console.log(`Query(Option): ${query}(${JSON.stringify(option)})`, r.success)
  }

The last case fails:

Query(Option): predicate_with_dict(_{a:d,b:e}, X)({}) true
Query(Option): predicate_with_dict(Input, X)({"Input":{"a":"d","b":"e"}}) true
Query(Option): predicate_with_dict(_{a:_,b:_}, X)({}) true
Query(Option): predicate_with_dict(Input, X)({"Input":{"a":"_","b":"_"}}) false

Thank you for your help!

This creates a Prolog term a:'_', which does not unify with a:b or a:d.

Typically you write your Prolog such that it is easy to call from JavaScript. If you really have to, SWI-Prolog -- Translating JavaScript data to Prolog tells you you can create a Prolog variable from the JavaScript term {s:‘v’}, which can be created using new Prolog.var(). So, you can write

{Input:{a:new Prolog.var(),b:new Prolog.var()}

But, I’d change predicate_with_dict/2. Dicts are fine to be passed both ways, but not if you want unification against them.

Thank you for your answer, I will think about how can I change the predicate to work better with JS.

We could be of some help if you describe how you plan to use this :slight_smile: In some cases, some idea about the scalability is useful as well. Describing “objects” using dicts to be used in the clause head is typically a bad idea as it leads to rather inefficient querying. Unimportant if you have a few, but important if you have millions. There are ways to deal with all of that, but the best solution depends on the details. For example, is the dict fully dynamic or is there a predefined set of keys?

Thanks, sure let me share more details, especially because I ran into another issue where I have a similar query but I wan to bind the possible dict values to variables.

So the problem I solve is determining trading card quality. In this particular case yugioh cards. I have 8 attributes: name, artwork, artwork_border, attribute_symbol,stars, lore_border, card_border, card_surface. Each attribute can have a different set of possible values. First I was using a list, but I ran into issues with some predicates where the actual and defined order was not the same. Of course it is possible I did something wrong. And I also felt the list just not descriptive enough. So these are the Dicts, and there are a bunch of possible rarities which is a unique combination of the settings of those 8 attributes.

The predicates are something like (it is different compared to the example above, mostly for readability, otherwise its hard to spot which fields are different. This was also a problem with the list solution):

card_rarity(Options,normal_rarity) :-
    print_message(information, format("                                   Card: ~w", Options)),
    valid_card(Base),
    Options = Base. 
card_rarity(Options,rare_rarity) :-
    valid_card(Base),
    member(Options, [Base.put(name,silver_holofoil), Base.put(name,black_holofoil)]).
card_rarity(Options,holofoil_rare_rarity):-
    valid_card(Base),
    Options = Base.put(_{artwork:holofoil_background,card_surface:holofoil}).
card_rarity(Options,ultra_rare_rarity):-
    valid_card(Base),
    Options = Base.put(_{name:gold_holofoil,artwork:holofoil_background}).

/* main_subject_coated_in_raised_glossy_varnish and embossed_holofoil_background might be always there */
card_rarity(Options,ultra_rare_rarity):-
    valid_card(B),
    put_dict(_{name:gold_holofoil, artwork_border:embossed_gold_holofoil, attribute_symbol:embossed_gold_holofoil, stars:embossed_gold_holofoil},B,Base),
    member(Options, [
        Base.put(_{artwork:embossed_holofoil_background, card_surface:sprakle}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, card_surface:sprakle}),
        Base.put(_{artwork:embossed_holofoil_background, lore_border:embossed_gold_holofoil, card_surface:sprakle}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:embossed_gold_holofoil, card_border:embossed_gold_holofoil, card_surface:sprakle}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:embossed_gold_holofoil, card_border:embossed_gold_holofoil, card_surface:sprakle}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:normal, card_border:embossed_gold_holofoil, card_surface:sprakle}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:normal, card_border:embossed_gold_holofoil, card_surface:sprakle}),
        Base.put(_{artwork:embossed_holofoil_background}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish}),
        Base.put(_{artwork:embossed_holofoil_background, lore_border:embossed_gold_holofoil}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:embossed_gold_holofoil, card_border:embossed_gold_holofoil}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:embossed_gold_holofoil, card_border:embossed_gold_holofoil}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:normal, card_border:embossed_gold_holofoil}),
        Base.put(_{artwork:main_subject_coated_in_raised_glossy_varnish, lore_border:normal, card_border:embossed_gold_holofoil})
    ]).
card_rarity(Options,collectors_rare_rarity):-
    valid_card(B),
    put_dict(_{name: rainbow_holographic, artwork_border:embossed_holofoil, attribute_symbol:embossed_sparkle, stars:embossed_sparkle, lore_border:embossed_holofoil,card_border: holographic_finger_printing, card_surface:sprakle},B,Base),
    member(Options, [
        Base.put(_{artwork:embossed_holofoil_background}), /* might be just one these two combined */
        Base.put(_{artwork:main_subject_embossed_in_holofoil})
    ]).

This also contains the biggest and worst. It is pretty explicit now, but as you can see it can be simplified by just ignoring a bunch of fields. For now it is like that so I can also get myself familiar with these things.

About the UI and the features:

It is a console app which shows you the attributes and their possible values where you can select, and as you select it shows you what are the matching rarities so far. Ideally you end up with one, or the unknown_rarity which is the catch all.
The reason why I ran into the new problem, I realized it would be cool if I can disable (or remove) options which have no chance to lead to a known rarity, so I decided to bind all unset attribute to a variable instead of the ignore and set the rest. BTW the prolog.Var() thing worked. Thank you

To answer your questions:

  • The dict is a relatively small set I think
  • It is not fully dynamic
  • the number of rarity predicates is 24

So, we describe a card using a bunch of attributes and we classify any card in one of 24 categories. Is that about right?

And, we want a predicate card_rarity(?Card, ?Rarity), i.e.,
we can give it a partial card and it will find all remaining possible rarities or a rarity and partial card and it will produce concrete cards that satisfy this rarity. Ok?

If that is true and our problem was a lot smaller, say just two properties with two values, we’d use

card_rarity(red, small, very_rare).
card_rarity(red, large, common).
card_rarity(green, small, abundant).
card_rarity(green, large, extremely_rare).

Now I can easily make all sensible queries. Right? Now we have 8 attributes. Depending on the number of possible values, this might get cumbersome. It surely is cumbersome to write.

If the above is a correct simplification though, we need to think of a pleasant representation of the rules. Next, we can think on how to implement it. So, my question is, how would you like writing the rules. Do not think about whether or not it can be executed, only on what is comfortable to write. You may use Prolog syntax, but English will do as well.

The attributes have 3-5 possible values, except for the artwork which has 13. Of course in reality not all permutation exists and most of the attributes for each rarities (not the same ones!) is set to normal (default value)

About the predicates, I tried (prolog pseudo code):

/* Pro: rather simple, query works Cons: very verbose, hard to see what are the differences, JS needs to know what are the possible attributes and their order */
card_rarity([silver_holofoil, normal, normal...], rare).
card_rarity([black_holofoil, normal, normal...], rare).


/* Pros: not sure if it has any compared to the previous Cons: same as above*/
card_rarity([Name, Artwork, AttributeSymbol...], rare) :-
    [silver_holofoil, normal, normal...];
    [black_holofoil, normal, normal...].

/*After this point I wanted to solve two problems: the client can query the attributes in a way and the delta of the rarities compared to the default one is easy to grasp. So I came up with this: */

/* js can use it to get a base object and the predicates can use it to set up the default (and js and prolog is on the same page about the attributes) */
valid_card(T) :-
    T = _{name:normal,artwork:normal,artwork_border:normal,attribute_symbol:normal,stars:normal,lore_border:normal,card_border:normal,card_surface:normal}.

/* Pros: predicates are short, even with the two flavor (using member or not) its easy to see the delta*/ 
card_rarity(Options,rare_rarity) :-
    valid_card(Base),
    member(Options, [Base.put(name,silver_holofoil), Base.put(name,black_holofoil)]).

card_rarity(Options,super_rare_rarity) :-
    valid_card(Base),
    Options = Base.put(artwork,holofoil_background).

card_rarity(Options,holofoil_rare_rarity):-
    valid_card(Base),
    Options = Base.put(_{artwork:holofoil_background,card_surface:holofoil}).

I am fine with the last way, the current length of the card_rarity predicates are less than 80 lines. There might be some pro solution which can remove even more “boilerplate” I just dont see how.

I did not mention, but the possible values for each attributes are defined in rules like:

name(normal).
name(silver_holofoil).
name(black_holofoil).
name(gold_holofoil).
name(rainbow_holographic).
name(silver_holographic).
name(gold_prismatic).

So the client can query the values as well and build the UI accordingly with some helper predicates like:

/* bad naming this is about the attributes */ 
identificationOption(T) :-
    valid_card(Default),
    dict_keys(Default, T).

get_values_of(P, Res) :-
    findall(O, call(P, O), Res).

get_all_values(Res) :-
    identificationOption(Keys),
    maplist(get_values_of, Keys, Values),
    pairs_keys_values(Pairs, Keys, Values),
    dict_create(Res, _, Pairs).

This is a rather peculiar way to fine a type of cards in my view. You basically describe the card type as a base type with modifications. I think it does work though.

I’d be tempted to write down a card-type as follows, where each list represents the allowed values and non-existing keys are simply not part of the description. On top of that you need a little bit of code to make it work the way you want or you can use this representation to generate something different.

card_rarity(#{key1: [value1,value2], key2:[value3], ...}, rarity).

I’d also consider a way to express a new rarity as a restriction of an already existing rarity class. The idea is that it is easier to deal with a data structure to do whatever you want that with a definition in terms of procedural steps such as member/2 and modifying dicts.

Together with your clean declaration of the admissible values for each of the properties, it is easy enough to write any query you want.

Thank you for your response, I will think about it how I can make it less procedural.