Monday, September 07, 2009

Drools Petstore Example in Prolog

I recently came across Prova, a Prolog dialect over the JVM using Mandarax as its inference engine. I have used SWI-Prolog before, but only as a simple inference engine for ontology relationships. Trying to develop an example in Prolog to try out with Prova, I realized that I couldn't code Prolog to save my life, so I set about trying to remedy that. This post is a result of that effort.

The example here is based on the Drools Pet Store Example. The Petstore in our simplified example sells only (a single unspecified species of) fish, fish food and fish tanks. Fish are $5/ea, food packets are $2/ea, and fish tanks are $40/ea. At checkout, some amount of upselling and customer coddling takes place, which are enshrined in the following rules:

  • Give customer free fish food, one packet for every 5 fish (s)he bought.
  • If customer has purchased fish, ask customer how much more fish food he wants. Add this to cart.
  • If customer has purchased 10 fish or more, and no tank to put them in, ask if (s)he wants a tank. If customer says yes, add to cart.
  • For orders over $50, apply a 10% discount.

The equivalent rules in Prolog are shown below. It is very likely that the code could have been more concise, had it been written by someone with more Prolog programming experience, but c'est la vie. The idea is that checkout goal would be invoked (possibly from a Java application) at the checkout phase, and it will be passed the shopping cart as a Prolog sequence. The cart can then be queried from the factbase using cart(X). Currently the code is written as a standalone Prolog application, it will probably require some changes when it is hooked up to a Java caller.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
% Source: petstore.pro

% A Cart is modelled as a sequence of:
% [#-fish, #-food, #-free-food, #-tank,discount_amount].
% The following block are rules to pull out components from the
% cart and populate the required value in the output parameter.
% @param Cart (IN) - the sequence representing the shopping cart.
% @param N (OUT) - the value of the named component.
number_of_fish([NumberOfFish, _, _, _, _], N) :- 
  N is NumberOfFish.
number_of_food([_, NumberOfFood, _, _, _], N) :- 
  N is NumberOfFood.
number_of_free_food([_, _, NumberOfFreeFood, _, _], N) :- 
  N is NumberOfFreeFood.
number_of_tank([_, _, _, NumberOfTank, _], N) :- 
  N is NumberOfTank.
discount([_, _, _, _, Discount], N) :- 
  N is Discount.

% Computes the contents of the cart using the predefined prices
% @param Cart (IN) - the sequence representing the shopping cart.
% @param Total (OUT) - the computed total dollar value of the cart.
compute_cart_total(Cart, Total) :-
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_tank(Cart, NumberOfTank),
  Total is (5 * NumberOfFish) + (2 * NumberOfFood) + (40 * NumberOfTank).

% if customer has 5 or more fish, give him free fish food,
% 1 fish food packet for every 5 fish he purchases.
add_free_fish_food :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  NewFreeFood is floor(NumberOfFish / 5),
  append([], [NumberOfFish, NumberOfFood, NewFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% ask customer if he wants additional fish food.
% @param Quantity (OUT) - the quantity of additional fish food.
ask_add_fish_food(Quantity) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  ask_add_fish_food(NumberOfFish, Quantity).
ask_add_fish_food(0, Quantity) :-
  Quantity is 0.
ask_add_fish_food(NumberOfFish, Quantity) :-
  NumberOfFish > 0,
  write('>> How much more fish food to add? '), 
  read(Quantity), nl.

% add additional fish food as requested by customer, to cart.
% @param Quantity (IN) - if 0, no action. If not 0, then add to cart
% and assert it back into the factbase.
add_fish_food(0) :- !.
add_fish_food(Quantity) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  NewFood is NumberOfFood + Quantity,
  append([], [NumberOfFish, NewFood, NumberOfFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% ask customer if he wants to buy a fish tank. Only ask the question
% if the number of fish are > 10 and customer doesn't already have a
% tank in his shopping cart.
% @param Yorn (OUT) - populated as a result of this method.
ask_add_fish_tank(Yorn) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_tank(Cart, NumberOfTank),
  ask_add_fish_tank(NumberOfFish, NumberOfTank, Yorn).
ask_add_fish_tank(NumberOfFish, _, Yorn) :-
  NumberOfFish < 10, Yorn is 0, !.
ask_add_fish_tank(_, NumberOfTank, Yorn) :-
  NumberOfTank > 0, Yorn is 0, !.
ask_add_fish_tank(_, _, Yorn) :-
  write('>> Add a fish tank? [y/n] '), read(Yorn), nl.

% Adds a fish tank to the cart and asserts it into the factbase.
% This is only done if the answer to the 'add a fish tank' question
% is 'y'.
add_fish_tank('y') :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  discount(Cart, Discount),
  Quantity is 1,
  append([], [NumberOfFish, NumberOfFood, NumberOfFreeFood, 
    Quantity, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).
add_fish_tank(_) :- !.

% if order value > $50, apply 10% discount on total
apply_discount :-
  cart(Cart),
  compute_cart_total(Cart, CartTotal),
  apply_discount(CartTotal).
apply_discount(CartTotal) :-
  CartTotal < 50, !.
apply_discount(CartTotal) :-
  CartTotal >= 50,
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  Discount is 0.1 * CartTotal,
  append([], [NumberOfFish, NumberOfFood, NumberOfFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% display contents of the cart.
% @param Heading (IN) - legend for the cart display
% @param Cart (IN) - the sequence representing the cart.
display_cart(Heading) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  compute_cart_total(Cart, CartTotal),
  write('==== '), write(Heading), write(' ===='), nl,
  write('#-Fish (@ $5/ea) = '), write(NumberOfFish), nl,
  write('#-Food (@ $2/ea) = '), write(NumberOfFood),
    write(' (Free: '), write(NumberOfFreeFood), write(')'), nl,
  write('#-Tank= (@ $40/ea) = '), write(NumberOfTank), nl,
  write('- Discount Given = ($'), write(Discount), write(')'), nl,
  Total is CartTotal - Discount,
  write('** Total = $'), write(Total), write(' **'), nl.

% The top level goal that is called from the client. It will prompt
% for the cart to be entered as a sequence, and print the invoice
% after applying all the rules to the shopping cart.
checkout :-
  retractall(cart(_)),
  write('Enter cart: '), read(Cart),
  assert(cart(Cart)),
  add_free_fish_food,
  ask_add_fish_food(MoreFishFood),
  add_fish_food(MoreFishFood),
  ask_add_fish_tank(AddFishTank),
  add_fish_tank(AddFishTank),
  apply_discount,
  display_cart('Invoice'),
  !.

Here are some examples of running the code. As you can see, different rules are fired based on the contents of the shopping cart.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
?- consult('petstore.pro').
% petstore.pro compiled 0.00 sec, 14,304 bytes
true.

?- checkout.
Enter cart: [5,0,0,0,0].    % 5 fish in cart
>> How much more fish food to add? 2.

==== Invoice ====
#-Fish (@ $5/ea) = 5
#-Food (@ $2/ea) = 2 (Free: 1)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($0)
** Total = $29 **
true.

?- cart(X).
X = [5, 2, 1, 0, 0].

?- checkout.
Enter cart: [10,0,0,0,0].   % 10 fish in cart, accept fish tank
>> How much more fish food to add? 2.

>> Add a fish tank? [y/n] y.

==== Invoice ====
#-Fish (@ $5/ea) = 10
#-Food (@ $2/ea) = 2 (Free: 2)
#-Tank= (@ $40/ea) = 1
- Discount Given = ($9.4)
** Total = $84.6 **
true.

?- cart(X).
X = [10, 2, 2, 1, 9.4].

?- checkout.
Enter cart: [10,0,0,0,0].   % 10 fish in cart, not accept fish tank
>> How much more fish food to add? 2.

>> Add a fish tank? [y/n] n.

==== Invoice ====
#-Fish (@ $5/ea) = 10
#-Food (@ $2/ea) = 2 (Free: 2)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($5.4)
** Total = $48.6 **
true.

?- cart(X).
X = [10, 2, 2, 0, 5.4].

?- checkout.
Enter cart: [0,10,0,0,0].   % fish food only
==== Invoice ====
#-Fish (@ $5/ea) = 0
#-Food (@ $2/ea) = 10 (Free: 0)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($0)
** Total = $20 **
true.

?- cart(X).
X = [0, 10, 0, 0, 0].

This is a somewhat contrived example, and its probably less work to actually bake these rules into the calling application as a bunch of if conditions. But it did help me to learn enough Prolog to write this... hopefully, I will be able to leverage this to write larger and more complicated rules in the future, and try this out in Prova with a Java caller.

No comments:

Post a Comment

Comments are moderated to prevent spam.