Ever looked into the kitchen cupboard and wondered what you can make for supper? Then this program is the answer. You first select the ingredients you have available:
It then tells you the recipes you can make:
You can add new recipes to the database, and update the list of ingredients.
The recipe program uses two global variables. The first one, ingredient-database, contains a list of all the ingredients we are going to use:
(defparameter ingredient-database '("eggs" "flour" "butter" "chicken" "beef" "pork"
"lamb" "sugar" "chocolate" "onions" "fish" "tomatoes" "pasta" "chorizo" "rice"
The second one, recipe-database, is going to contain a list of the recipes. Each recipe is a list of three items:
- The name of the recipe.
- A list of the ingredients.
- A short description of the method.
So after a couple of recipes have been added, recipe-database might look like this:
(("Cheese omelette" ("eggs" "cheese")
"Beat the eggs, cook, and add the cheese")
("Pizza" ("flour" "tomatoes" "chorizo" "cheese")
"Make a dough, add tomato, cheese, and chorizo, and bake for 12m at 210°C"))
Our starting point is a procedure contains to test whether an ingredient is contained in a list of ingredients. We will use it like this:
CL-USER > (contains "chicken" '("pork" "chicken" "rice")) T
CL-USER > (contains "chicken" '("pork" "chocolate" "rice")) NIL
To see if an ingredient is in a list of ingredients:
- If the list is empty then the answer is no.
- If the ingredient is the first item on the list then the answer is yes.
- Otherwise it's the answer to the question - is the ingredient in the rest of the ingredients excluding the first element?
Here's the definition as a Lisp procedure:
(defun contains (item list) (if (null list) nil (if (string= item (first list)) t (contains item (rest list)))))
Based on this we define a procedure subset that checks whether a list of ingredients lista is a subset of the list listb. We will use this as follows:
CL-USER > (subset '("pork" "rice") '("pork" "eggs" "rice")) T
CL-USER > (subset '("pork" "eggs" "rice") '("pork" "rice")) NIL
In English this is defined as follows:
To check whether lista is a subset of listb
- If lista is empty then the answer is yes.
- If listb doesn't contain the first element of lista then the answer is no
- Otherwise it's the answer to the question - is the rest of lista excluding the first element a subset of listb?
As a Lisp procedure it becomes:
(defun subset (lista listb) (if (null lista) t (if (null (contains (first lista) listb)) nil (subset (rest lista) listb))))
The recipe program
Now we're going to define the procedure that finds all the recipes you can make with a particular set of ingredients. We will call it like this:
CL-USER > (recipes-can-make '("cheese" "eggs") recipe-database) (("Cheese omelette" ("eggs" "cheese") "Beat the eggs, cook, and add the cheese"))
The definition in English is:
To find the recipes you can make with the ingredients:
- If the list of recipes is empty then answer none.
- If the first recipe's ingredients are a subset of the list of available ingredients, return that recipe plus the result of checking the remaining recipes.
- Otherwise return just the result of checking the remaining recipes.
Here's the procedure in Lisp:
(defun recipes-can-make (ingredients recipes) (if (null recipes) nil (let* ((entry (first recipes)) (needs (second entry))) (if (subset needs ingredients) (cons entry (recipes-can-make ingredients (rest recipes))) (recipes-can-make ingredients (rest recipes))))))
The user interface
Finally we add some dialogue boxes to make adding and looking up recipes easier. Here's a procedure for adding a recipe:
(defun add-recipe () (let ((name (capi:prompt-for-string "What's the recipe?")) (ingredients (capi:prompt-for-items-from-list ingredient-database "What does it need?")) (method (capi:prompt-for-string "Brief method:"))) (setq recipe-database (cons (list name ingredients method) recipe-database))))
Now here's the interface for looking up a recipe:
(defun find-recipe () (let ((ingredients (capi:prompt-for-items-from-list (sort ingredient-database #'string<) "What ingredients do you have?"))) (capi:prompt-with-list (recipes-can-make ingredients recipe-database) "You can make these:")))
Saving and loading the databases
Finally, here are procedures to save the databases to a file on disk:
(defun save-recipes () (with-open-file (stream "Recipes" :direction :output :if-exists :supersede) (write ingredient-database :stream stream) (write recipe-database :stream stream)))
and load them back in:
(defun load-recipes () (with-open-file (stream "Recipes" :direction :input) (setf ingredient-database (read stream)) (setf recipe-database (read stream))))
blog comments powered by Disqus