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.