Recipes
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.
Description
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"
"potatoes" "cheese"))
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"))
Basic routines
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
but:
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
But:
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
