LPA Home Page LPA Home Page
Products Support Download About LPA Hot News Search

ProWeb: Choose a Meal

Prolog is an ideal language for generating alternative solutions to a given set of criteria. This example demonstrates a meal menu generation program.

The Underlying Prolog Program

The basis of this example is the MEALS.PL program, distributed with WIN-PROLOG, which computes meals of a particular type from an internal database of dishes. This example is used by LPA to show how a single 'logical' Prolog program can not only be used in conjunction with a Prolog front-end but also with an external language front-end such as C, Visual BASIC or Java and, in the case of this example, with an HTML front-end distributed over the Internet or an intranet.

A meal consists of some combination of a starter, a main course, a dessert and some wine. The basic type of meal is 'any meal', which justs returns each possible combination of the above. The program builds-up five other types of meal simply by introducing additional tests that need to be satisfied, such as compatibility between courses, price or calorie count. The following clause allows us to select meals of a particular type together with their cost and energy. Note that this is a replacement for the menu/1 predicate defined within MEALS.PL.

pw_menu( Meal, Starter, Main, Dessert,
         Wine, Cost, Calories ) :-
   Meal( Starter, Main, Dessert, Wine ),
   cost( Starter, Main, Dessert, Wine, Cost ),
   energy( Starter, Main, Dessert, Wine, Calories ).
If you were to execute pw_menu/7 from the Prolog prompt, a solution similar to the following would be returned:

?- pw_menu( any_meal, Starter, Main, Dessert,
            Wine, Cost, Calories ). <enter>

Starter = `Prawn Cocktail` ,
Main = `Dover Sole` ,
Dessert = `Chocolate Fudge Cake` ,
Wine = `Chablis` ,
Cost = 30.63225 ,
Calories = 1000
Backtracking through the goal causes successive solutions to be returned:

Starter = `Prawn Cocktail` ,
Main = `Dover Sole` ,
Dessert = `Chocolate Fudge Cake` ,
Wine = `Muscadet Sur Lie` ,
Cost = 23.5235 ,
Calories = 975 ;

...
The first argument of pw_menu/7, Meal, is a variable which can take on the value any_meal, good_meal, diet_meal, glutton_meal, yuppie_meal or cheap_meal. For each type of meal, there is a clause of the same name and this argument gets used as the predicate name (see Meal/4 above). The definition of any_meal/4 is as follows:

% any meal consists of a starter, main dish, dessert and wine

any_meal( Starter, Main, Dessert, Wine ) :-
  starter( Starter ),
  main( Main ),
  dessert( Dessert ),
  wine( Wine ).
This simple clause links directly to the internal database of dishes, namely the predicate starter/1, main/1, dessert/1 and wine/1:

% these are the starters

starter( 'Prawn Cocktail' ).
starter( 'Pate Maison' ).
starter( 'Avocado Vinaigrette' ).
starter( 'Stuffed Mushrooms' ).
starter( 'Parma Ham With Melon' ).
starter( 'Asparagus Soup' ).

% and here are the main courses (vegetables are included!)

main( 'Dover Sole' ).
main( 'Fillet Steak' ).
main( 'Calves Liver' ).
main( 'Chicken Kiev' ).
main( 'Ragout Of Lamb' ).
main( 'Poached Salmon' ).

% the desserts follow here

dessert( 'Chocolate Fudge Cake' ).
dessert( 'Vanilla Ice Cream' ).
dessert( 'Peach Melba' ).
dessert( 'Waffles With Maple Syrup' ).
dessert( 'Fresh Fruit Salad' ).
dessert( 'Apple And Blackberry Pie' ).

% and these are the wines

wine( 'Chablis' ).
wine( 'Muscadet Sur Lie' ).
wine( 'Beaujolais Nouveau' ).
wine( 'Nuits Saint George' ).
wine( 'Gewurztraminer' ).
wine( 'Cabernet Shiraz' ).
Running any_meal/4 from the Prolog prompt gives the following results:

?- any_meal( Starter, Main, Dessert, Wine ). <enter>

Starter = `Prawn Cocktail` ,
Main = `Dover Sole` ,
Dessert = `Chocolate Fudge Cake` ,
Wine = `Chablis`
Backtracking through the goal causes successive solutions to be returned:

Starter = `Prawn Cocktail` ,
Main = `Dover Sole` ,
Dessert = `Chocolate Fudge Cake` ,
Wine = `Muscadet Sur Lie`

...
With the any_meal/4 option, certain meals are no good because the starter and the main course are too similar or the main course and the wine do not go together. If you were to select the good_meal/4 option, meals suffering with such problems would not be returned.

% in good meal the starter and wine are ok with the main dish

good_meal( Starter, Main, Dessert, Wine ) :-
  any_meal( Starter, Main, Dessert, Wine ),
  starter_ok( Starter, Main ),
  wine_ok( Wine, Main ).
The starter_ok/2 and wine_ok/2 predicates are as shown below. A starter is OK if it is not the same type of dish as the main course. As for the wine, a white wine is OK with fish and poultry and red is for meat.

% a starter is ok if it is a different type to the main course

starter_ok( Starter, Main ) :-
  dish_type( Starter, Type ),
  not dish_type( Main, Type ).

% white wine is ok with fish and poultry, and red with meat

wine_ok( Wine, Main ) :-
  colour( Wine, white ),
  dish_type( Main, poultry ).

wine_ok( Wine, Main ) :-
  colour( Wine, white ),
  dish_type( Main, fish ).

wine_ok( Wine, Main ) :-
  colour( Wine, red ),
  dish_type( Main, meat ).
These refer to two deeper database predicates, colour/2 and dish_type/2. The colour/2 predicate simply lists the colours of the various wines, while dish_type/2 classifies starters and main courses as one of the dish types poultry, vegetable, meat or fish.

% here is a list of the colours of the various wines

colour( 'Beaujolais Nouveau', red ).
colour( 'Nuits Saint George', red ).
colour( 'Cabernet Shiraz', red ).
colour( 'Chablis', white ).
colour( 'Muscadet Sur Lie', white ).
colour( 'Gewurztraminer', white ).

% this classifies the ingredients in each starter and main dish

dish_type( 'Pate Maison', poultry ).
dish_type( 'Chicken Kiev', poultry ).
dish_type( 'Avocado Vinaigrette', vegetable ).
dish_type( 'Stuffed Mushrooms', vegetable ).
dish_type( 'Asparagus Soup', vegetable ).
dish_type( 'Parma Ham With Melon', meat ).
dish_type( 'Fillet Steak', meat ).
dish_type( 'Calves Liver', meat ).
dish_type( 'Ragout Of Lamb', meat ).
dish_type( 'Prawn Cocktail', fish ).
dish_type( 'Dover Sole', fish ).
dish_type( 'Poached Salmon', fish ).
Price is one criterion that can be placed on the selection of a meal. The meal types cheap_meal/4 and yuppie_meal/4 use this to select a meal costing less than £15 or more than £30 respectively:

% a cheap meal is any meal costing less than £15

cheap_meal( Starter, Main, Dessert, Wine ) :-
  any_meal( Starter, Main, Dessert, Wine ),
  cost( Starter, Main, Dessert, Wine, Cost ),
  Cost < 15.

% a yuppie meal must be a good meal costing more than £30

yuppie_meal( Starter, Main, Dessert, Wine ) :-
  good_meal( Starter, Main, Dessert, Wine ),
  cost( Starter, Main, Dessert, Wine, Cost ),
  Cost > 30.
The cost/5 predicate to which they refer is defined below. This combines the price for each of the starter, the main course, the dessert and the wine and then adds 17.5% for Value Added Tax and 10% for service charge.

% the meal's cost is the price plus 17.5% vat and 10% service

cost( Starter, Main, Dessert, Wine, Cost ) :-
  price( Starter, Prc1 ),
  price( Main, Prc2 ),
  price( Dessert, Prc3 ),
  price( Wine, Prc4 ),
  Cost is ( Prc1 + Prc2 + Prc3 + Prc4 ) * 1.175 * 1.10.

% the meal item prices (before service and vat) are listed here

price( 'Chablis', 10.95 ).
price( 'Muscadet Sur Lie', 5.45 ).
price( 'Beaujolais Nouveau', 4.75 ).
price( 'Nuits Saint George', 12.75 ).
price( 'Gewurztraminer', 9.25 ).
price( 'Cabernet Shiraz', 8.65 ).
price( 'Chocolate Fudge Cake', 1.75 ).
price( 'Vanilla Ice Cream', 0.95 ).
price( 'Peach Melba', 1.55 ).
price( 'Waffles With Maple Syrup', 1.35 ).
price( 'Fresh Fruit Salad', 1.95 ).
price( 'Apple And Blackberry Pie', 1.75 ).
price( 'Dover Sole', 8.75 ).
price( 'Fillet Steak', 6.50 ).
price( 'Calves Liver', 4.95 ).
price( 'Chicken Kiev', 3.65 ).
price( 'Ragout Of Lamb', 5.25 ).
price( 'Poached Salmon', 7.25 ).
price( 'Prawn Cocktail', 2.25 ).
price( 'Pate Maison', 1.45 ).
price( 'Avocado Vinaigrette', 1.55 ).
price( 'Stuffed Mushrooms', 1.15 ).
price( 'Parma Ham With Melon', 2.75 ).
price( 'Asparagus Soup', 1.95 ).
Another criterion for selecting a meal is the number of calories it contains. The meal types diet_meal/4 and glutton_meal/4 select meals with less than 650 calories or more than 1500 calories respectively. Note that in the case of glutton_meal/4, a good meal is selected before the calories are counted.

% a diet meal is any meal with fewer than 650 calories

diet_meal( Starter, Main, Dessert, Wine ) :-
  any_meal( Starter, Main, Dessert, Wine ),
  energy( Starter, Main, Dessert, Wine, Calories ),
  Calories < 650.

% a glutton meal must be a good one with over 1500 calories

glutton_meal( Starter, Main, Dessert, Wine ) :-
  good_meal( Starter, Main, Dessert, Wine ),
  energy( Starter, Main, Dessert, Wine, Calories ),
  Calories > 1500.
The energy/5 database predicate gives the number of calories in a meal calculated from the total energy of the four parts as obtained from the calories/2 predicate; this lists each meal item's energy content in kilocalories.

% the calories in a meal is the total energy of the four parts

energy( Starter, Main, Dessert, Wine, Calories ) :-
  calories( Starter, Cal1 ),
  calories( Main, Cal2 ),
  calories( Dessert, Cal3 ),
  calories( Wine, Cal4 ),
  Calories is Cal1 + Cal2 + Cal3 + Cal4.

% the energy content of each item is listed here in calories

calories( 'Chablis', 125 ).
calories( 'Muscadet Sur Lie', 100 ).
calories( 'Beaujolais Nouveau', 150 ).
calories( 'Nuits Saint George', 225 ).
calories( 'Gewurztraminer', 125 ).
calories( 'Cabernet Shiraz', 200 ).
calories( 'Chocolate Fudge Cake', 450 ).
calories( 'Vanilla Ice Cream', 325 ).
calories( 'Peach Melba', 375 ).
calories( 'Waffles With Maple Syrup', 425 ).
calories( 'Fresh Fruit Salad', 175 ).
calories( 'Apple And Blackberry Pie', 250 ).
calories( 'Dover Sole', 250 ).
calories( 'Fillet Steak', 650 ).
calories( 'Calves Liver', 425 ).
calories( 'Chicken Kiev', 450 ).
calories( 'Ragout Of Lamb', 500 ).
calories( 'Poached Salmon', 225 ).
calories( 'Prawn Cocktail', 175 ).
calories( 'Pate Maison', 150 ).
calories( 'Avocado Vinaigrette', 275 ).
calories( 'Stuffed Mushrooms', 200 ).
calories( 'Parma Ham With Melon', 100 ).
calories( 'Asparagus Soup', 125 ).

Developing the ProWeb Front-End

This program initially asks you to select a meal type; upon submission, meals that fit the simple rules defining the type are generated from an internal database of dishes. Each solution is then presented in the form of an HTML page.

The main goal for this ProWeb example is meals/0, as shown below. Successive solutions to the pw_menu/7 predicate are generated using the built-in solution/2 predicate. If the page containing the Nth solution has already been sent, the next line fails and forces the solution/2 predicate to backtrack and generate the next numbered solution. In executing each of the proweb_post_reply predicates, ProWeb stores another piece of the solution. The last line constructs the meals(output(Meal,Nth)) page and sends it to the client.

meals :-
   proweb_send_form( meals(input) ),
   proweb_returned_answer( meals_type, Meal ),
   meal_type( Meal, Goal, _ ),
   solution(
     pw_menu(Goal,Starter,Main,Dessert,Wine,Cost,Calories),
     Nth
           ),
   \+ proweb_returned_form( meals(output(_,Nth)) ),
   gif( Starter, StarterGif ),
   gif( Main, MainGif ),
   gif( Dessert, DessertGif ),
   gif( Wine, WineGif ),
   proweb_post_reply( cost,         money(£,'s,.') @ Cost ),
   proweb_post_reply( calories,     Calories ),
   proweb_post_reply( starter_name, Starter ),
   proweb_post_reply( starter_gif,  StarterGif ),
   proweb_post_reply( main_name,    Main ),
   proweb_post_reply( main_gif,     MainGif ),
   proweb_post_reply( dessert_name, Dessert ),
   proweb_post_reply( dessert_gif,  DessertGif ),
   proweb_post_reply( wine_name,    Wine ),
   proweb_post_reply( wine_gif,     WineGif ),
   proweb_send_form( meals(output(Meal,Nth)) ).

% when there are no more solutions send the finish form

meals :-
   proweb_send_form( meals(finish) ).
In the ProWeb front-end, we have chosen to display each meal on a separate HTML page, broken down so as to show the four constituents of the meal, its cost and calorie content. In all, our example uses three HTML pages, as now described.

The meals(input) page asks you to select the meal type required; selection is via one or six radio buttons. The meals(input) page is defined as follows:

proweb_page( [ Form ],
             [ include('meals\head.htm'),
               Title,
               include('meals\body.htm'),
               proweb(Form),
               include('meals\foot.htm')
             ]
           ) :-
  (
    Form  = meals(input),
    Title = `Meals Example - Choose a Meal`
  ;
    Form  = meals(finish),
    Title = `Meals Example - No More Meals`
  ).

proweb_form( meals(input),
                  include('\inetpub\pws\data\pws\html\MEALS1.HTM')
                ).

% set all the meal types in a question

proweb_question( meals_type,
                 [method = radio,
                  select = Select,
                  prefill = 'Any Meal'
                 ]
               ) :-
   findall( [Type,font(size = 3) @ i @ TypeText],
            meal_type( Type, _, TypeText ),
            Select
          ).

% The meal type/goal/description database

meal_type( 'Any Meal',
           any_meal,
           'Any starter, main course, dessert and wine' ).
meal_type( 'Good Meal',
           good_meal,
           'The starter, main course and wine are good
            together' ).
meal_type( 'Cheap Meal',
           cheap_meal,
           'Any meal costing less than £15.00' ).
meal_type( 'Yuppie Meal',
           yuppie_meal,
           'A good meal costing more than £30.00' ).
meal_type( 'Diet Meal',
           diet_meal,
           'Any meal with less than 750 calories' ).
meal_type( 'Glutton Meal',
           glutton_meal,
           'A good meal with more than 1500 calories' ).
The text in each of the respective files, HEAD.HTM, BODY.HTM and FOOT.HTM, is as follows:

<HTML>
  <HEAD>
    <TITLE>

title gets inserted here...

    </TITLE>
  </HEAD>
  <BODY BACKGROUND="/lpa_back.jpg"
      BGCOLOR="#FFFFFF" TEXT="#000000"
      LINK="#0000FF" VLINK="#FF0000" ALINK="#00FF00">
    <TABLE>
      <TD>
      </TD>
      <TD>

form gets inserted here...

      </TD>
    </TABLE>
  </BODY>
</HTML>
The text of the template MEALS1.HTM file utilised by the meals(input) page is:
<HTML>
<HEAD>
<TITLE>
ProWeb Server - Meals Example - Input Source
</TITLE>
</HEAD>
<BODY>
<FORM>
<H3>Please select your menu choice ...</H3>
  <TABLE BORDER="3">
    <PROWEB QUESTION="meals_type">
  </TABLE>
<P>
<INPUT TYPE="submit"
    VALUE="Display Meal Selections"><P>
<A HREF="/pws_det.htm">Back to ProWeb Demos</A><P>
<A HREF="/index.htm">
    Visit the LPA Home Page!</A><P>
</FORM>
</BODY>
</HTML>
The body attributes are taken from the BODY.HTM file:

<BODY BACKGROUND="/lpa_back.jpg"
    BGCOLOR="#FFFFFF" TEXT="#000000"
    LINK="#0000FF" VLINK="#FF0000" ALINK="#00FF00">
</BODY>
It is the meals(output(Meal,_)) page that presents each meal selection. This page is created from a combination of two prolog clauses and a template HTML file; the prolog clauses are:

% define the background for all the meals forms

proweb_page( [ Form ],
             [ include('meals\head.htm'),
               Title,
               include('meals\body.htm'),
               proweb(Form),
               include('meals\foot.htm')
             ]
           ) :-
  Form = meals(output(Meal,_)),
  write( Meal ) ~> MealString,
  cat( [`Meals Example - `,MealString,` Menu`], Title, _ ).

% display a menu solution

proweb_form( meals(output(_,_)),
             include('\inetpub\pws\data\pws\html\MEALS2.HTM')
           ).
The text of MEALS2.HTM is as follows:

<HTML>
<HEAD>
<TITLE>ProWeb Server - Meals Example - Output Source</TITLE>
</HEAD>
<BODY>
<FORM>
<CENTER>
<TABLE COLS=3 BORDER=4
    CELLSPACING=4 CELLPADDING=4 WIDTH="100%">
<THEAD>
  <TR>
    <TD ALIGN=CENTER COLSPAN=3>
      <H2>Today's delicious choice of
       <PROWEB REPLY="meals_type"> menus</H2>
    </TD>
  </TR>
</THEAD>
<TBODY>
  <TR>
    <TD ALIGN=CENTER>
      <IMG SRC="IMAGES\
        <PROWEB REPLY="starter_gif">" WIDTH=128 HEIGHT=128>
      <BR>
      <FONT SIZE=3><PROWEB REPLY="starter_name">
      </FONT>
    </TD>
    <TD ALIGN=CENTER>
      <IMG SRC="IMAGES\
        <PROWEB REPLY="main_gif">" WIDTH=128 HEIGHT=128>
      <BR>
      <FONT SIZE=3><PROWEB REPLY="main_name">
      </FONT>
    </TD>
    <TD ALIGN=CENTER>
       <FONT SIZE=5>
         <PROWEB REPLY="cost">
         <P>
         <PROWEB REPLY="calories"> Calories
       </FONT>
    </TD>
  </TR>
  <TR>
    <TD ALIGN=CENTER>
      <IMG SRC="IMAGES\
      <PROWEB REPLY="dessert_gif">" WIDTH=128 HEIGHT=128>
      <BR>
      <FONT SIZE=3><PROWEB REPLY="dessert_name">
      </FONT>
    </TD>
    <TD ALIGN=CENTER>
      <IMG SRC="IMAGES\
      <PROWEB REPLY="wine_gif">" WIDTH=128 HEIGHT=128>
      <BR>
      <FONT SIZE=3><PROWEB REPLY="wine_name">
      </FONT>
    </TD>
    <TD ALIGN=CENTER>
       <FONT SIZE=4>
         <INPUT TYPE=SUBMIT
             VALUE="Get Next Meal Selection">
         <P>
         <A HREF="/pws_det.htm">
             Back to ProWeb Demos</A><P>
         <A HREF="/index.htm">
             Visit the LPA Home Page!</A><P>
       </FONT>
     </TD>
  </TR>
</TBODY>
</TABLE>
</CENTER>
</FORM>
</BODY>
</HTML>
To improve the appearance of the second page, four GIF files are sent, one for each of the starter, main course, dessert and wine. The following database predicate converts from the textual name to the GIF file name:

% The meal description/GIF file database

gif( `Prawn Cocktail`, 'PRAWNS.GIF' ).
gif( `Pate Maison`, 'PATE.GIF' ).
gif( `Avocado Vinaigrette`, 'AVOCADO.GIF' ).
gif( `Stuffed Mushrooms`, 'MUSHROOM.GIF' ).
gif( `Parma Ham With Melon`, 'MELON.GIF' ).
gif( `Asparagus Soup`, 'SOUP.GIF' ).
gif( `Dover Sole`, 'SOLE.GIF' ).
gif( `Fillet Steak`, 'STEAK.GIF' ).
gif( `Poached Salmon`, 'SALMON.GIF' ).
gif( `Calves Liver`, 'LIVER.GIF' ).
gif( `Chicken Kiev`, 'CHICKEN.GIF' ).
gif( `Ragout Of Lamb`, 'LAMB.GIF' ).
gif( `Chocolate Fudge Cake`, 'CAKE.GIF' ).
gif( `Vanilla Ice Cream`, 'ICECREAM.GIF' ).
gif( `Waffles With Maple Syrup`, 'WAFFLE.GIF' ).
gif( `Apple And Blackberry Pie`, 'PIE.GIF' ).
gif( `Peach Melba`, 'PEACH.GIF' ).
gif( `Fresh Fruit Salad`, 'FRUITS.GIF' ).
gif( Wine, ColourGif ) :-
   colour( Wine, Colour ),
   cat( [Colour,'.GIF'], ColourGif, _ ).
The job of the meals(finish) page is to tell you that no more solutions can be found; this is defined as:

% terminate the example

proweb_form( meals(finish),
             include('\inetpub\pws\data\pws\html\MEALS3.HTM')
           ).
The text of MEALS3.HTM is:

<HTML>
<HEAD>
<TITLE>ProWeb Server - Meals Example - Finish Source</TITLE>
</HEAD>
<BODY>
<FORM>
  <H1>There are no more suggestions</H1>
  <P>
  <A HREF="/pws_det.htm">
      Back to ProWeb Demos</A><P>
  <A HREF="/index.htm">
      Visit the LPA Home Page!</A><P>
</FORM>
</BODY>
</HTML>