4
votes

as I got critic concerning my data structure in Prolog I asked here the experts for alternative solutions.

As example, I have a data set of recipe decription in XML

<recipeml fileversion="13.8.2014">
  <recipe>
    <head>
      <title>Green Soup</title>
    </head>
    <ing-div type="titled">
        <title>soup</title>
        <ingredients>
          <ing>
            <amt><qty>500</qty><unit>gramm</unit></amt>
            <item>pea</item>
          </ing>
          <ing>
            <amt><qty>200</qty><unit>ml</unit></amt>
            <item>cream</item>
          </ing>
          ...
        </ingredients>
    </ing-div>  
    <directions>
      <step>Do something, cooking ....</step>
      <step>Next do again something...</step>
      ...
    </directions>
  </recipe>
  <recipe>
   ...
  </recipe>
  ...
</recipeml>

I choose to store it in Prolog as iterative element-tree using lists:

database([element('recipeml',[version=0.5], 
    [element('recipe',[],
        [element('head',[],
            [element('title',[],['Green Soup']
            )]
        ),
        element('ing-div',[type=titled], 
            [element('title',[],['soup']),
             element('ingredients',[],
                [element(ing,[],
                    [ element(amt,[],
                        [ element(qty,[],['500']), element(unit,[],[gramm]),]),
                    element(item,[],['pea']) 
                    ]),
                element(ing,[],
                    [ element(amt,[],
                        [ element(qty,[],['200']), element(unit,[],[ml]),]),
                    element(item,[],['pea']) 
                    ])
                ]
            )]
        )]
    ),
    element('recipe',[],...
    )] 
)]).

What I want to do is to look easily for recipes based on user input. The user might give an ingredient or part of the recipe name as input.

Actually I ran throught the elements by

ask_element(Name, Child, Parent) :-
        (
            member( element(Name,_,Child),Parent)
        ;
            member( element(_,_,NewParent),Parent),
            [_|_] = NewParent,
            ask_element(Name, Child, NewParent)
        ).

I got all recipes with a special ingredient by

 findall(RTitle,
            (
            ask_element('recipe',RKnot,Knot),
            ask_element('item',TmpIng,RKnot),
            contains(TmpIng,Ingredient),
            [Ing|_] = TmpIng, % avoid brackets [Egg]
            define_xml_knot(['head','title'],_,RKnot,TmpRTitle),
            [RTitle|_] = TmpRTitle % avoid brackets [Soup]
        ,Bag),

My result is then a list of the recipes titles. If a list of ingredients is entered I need a second analysing step to get the recipe with the most matching ingredients. Maybe this is not really Prolog style?

One idea, following the remark by Paulo Moura (thanks), is to arrange the data as

recipe(IDnumber,'Green Soup',ingredients(item(500,gramm,'pea'),item(200,ml,'cream')),steps('Do something','Next step do again something')).

I am not sure if this would be really help. Looking for a recipe with a certain ingredient I have to look again step by step in each recipe through every item if the ingredient I am looking for (or part of the word) is contained. And if I want to add a new descriptor, e.g. "level(easy)" I have to change all data calls as the number of element in recipe() changes. With the element(element...) construction I do not have to change the calls. But the response would be better, by returning only the IDnumber, then I get the whole recipe in one "call" (recipe(123,X,Y,Z)) for further processing. Actually I return as response "string text in a list" as you see it in "Bag" above...

It is my first application in Prolog, so I am not very familiar with an adquate data storing. I would be grateful for every hint.

2
In my suggestion, ingredients and steps would have a single argument, a list of, respectively, ingredients and steps. I.e. using your example, recipe(IDnumber,'Green Soup',ingredients([item(500,gramm,'pea'),item(200,ml,'cream')]),steps(['Do something','Next step do again something'])).. Having ingredients and steps as compound terms with recipe-depending arity would just complicate processing for no benefit. This will also mirror more closely the XML structure you present (assuming that's your intention).Paulo Moura

2 Answers

2
votes

SWI-Prolog offers library(xpath), that allows referencing nodes and properties 'Prolog style'. On backtracking the instantiations are returned to caller. So you can use in findall etc as you see best fit.

?- database(Db), xpath(Db, //recipe, Recipe).

will enumerate all recipes. The library is powerful, but not easy to learn. Look at the (meager) examples you see there...

You can also look here, I answered suggesting library(xpath) to process GCC XML. I used it to build my SWI / OpenGL interface...

1
votes

If you want to access from Prolog information represented in XML files, Carlo's solution is a good one.

But let's assume that you want all recipes represented in Prolog. One solution, as you described, is to use one fact per recipe, with a structure that best fits the most common data access patterns in your application. As you also noticed, looking for e.g. recipes using a specific ingredient or requiring a specific test will not be efficient as you must go from recipe fact to list of ingredients (or steps) and then do a linear search on that list (you could use a binary search tree instead of a list but I doubt the likely low number of items would justify it computationally). Moreover, adding new descriptors like level/1 in your question requires changes to be potentially propagated to all code that accesses the recipe data. Taking this issues into account, it might be worth to look into use a module or an object representation for the recipes. The idea would be that each recipe would be represented by a module or object with one predicate per attribute. with this representation, the computational cost of accessing an ingredient would be the same as the cost of accessing the recipe name or one of its steps. When e.g. searching for recipes with a specific ingredient, the necessary step of enumerating all recipe modules or objects is a cheap operation. Adding a new descriptor is easy with an object representation and can also be hacked with a module representation (essentially, you would simply modify the recipe interface, possibly adding a default value for the new descriptor). It's also possible to have a mixed representation and there are cases where such a solution is justified. It would be easier to advice if you share more details on the access or reasoning that you to apply to your recipes database.

Update: An example, based on the Logtalk object-oriented extension to Prolog (which you can use with most Prolog implementations, including GNU Prolog and SWI-Prolog). Several variations are possible. For hacking the concept of interface/protocol using instead modules see e.g. this post.

:- protocol(recipep).

    :- public([
        name/1, ingredient/3, step/1         % descriptors
    ]).

:- end_protocol.


:- object(proto_recipe, implements(recipep)).

    :- public([
        ingredient/1, ingredients/1, steps/1  % utility predicates
    ]).

    ingredient(Ingredient) :-
        ::ingredient(Ingredient,_,_).

    ingredients(Ingredients) :-
        findall(Ingredient, ::ingredient(Ingredient,_,_), Ingredients).

    steps(Steps) :-
        findall(Step, ::step(Step), Steps).

:- end_object.


:- object(green_soup, extends(proto_recipe)).

    name('Green Soup').

    ingredient(pea, 500, gr).
    ingredient(cream, 200, ml).

    step(...).
    ...

:- end_object.


:- object(mashed_peas, extends(proto_recipe)).

    name('Mashed Peas').

    ingredient(pea, 700, gr).
    ingredient(salt, 20, gr).
    ...

:- end_object.

Sample queries:

?- green_soup::ingredients(Ingredients).
Ingredients = [pea, cream].

?- conforms_to_protocol(Recipe, recipep), Recipe::ingredient(pea).
Recipe = green_soup ;
Recipe = mashed_peas ;
false.

Now assume that later you want to add a level/1 descriptor to all recipes. Just for fun, let's use hot patching:

:- category(add_recipe_level_descriptor, complements(proto_recipe)).

    :- public(level/1).
    :- dynamic(level/1).

:- end_category.

You can now add your cooking experience. E.g. you always get into trouble while making green soup:

?- green_soup::assertz(level(hard)).
true.

But most recipes are easy, so let's add a default value to all recipes:

:- category(recipe_level_default_value, complements(proto_recipe)).

    level(easy).

:- end_category.

Now you can ask:

?- mashed_peas::level(Level).
Level = easy.

I have omitted some details (e.g. setup and compilation/loading steps) but hopefully this gives you an idea of what's possible (but full running example here).