An inductive model of food
At long last I combine my two passions to form an unlikely duo: mathematical rigour and bodybuilding.
For me, the biggest challenge in bodybuilding has always been to eat enough. The apps out there for tracking macros and meal planning always left me dissastisfied, so I even made my own called macro-traco. But in macro-traco, I made the same mistake that major apps like MyFitnessPal make: my model of food considered as distinct a number of things that are, at their core, not different at all. Those are foods, recipes, meals, meal plans/journals. Addressing that mistake is what led me to implement Nutcalc in a very natural way.
Getting to the bottom of food
What is a meal plan? It’s a schedule of meals to eat in a day. Really it’s the same as a meal journal, but with a forward-thinking perspective. So a meal plan (or journal) is composed of meals.
What is a meal? (I promise this isn’t a silly question.) Take the bodybuilder classic: chicken, broccoli and rice. In this case, the meal is composed of three foods. Take a less simple meal: pasta with bolognese sauce. Whereas pasta is just a food, bolognese sauce is a recipe you make yourself, unless of course you buy it in a jar.
Then, a recipe is something that doesn’t have a nutrition facts label, that you make yourself by combining various foods via a mysterious process known as “cooking.”
Finally, what is a food? A food, practically speaking, has a “nutrition facts” label on it, listing the nutrients of the food.
At last we reach the bottom of this model: nutrients sit indivisible, at the bottom of this hierarchy.
To recap, this exploration reveals the following levels, from largest to smallest: a meal plan/journal is composed of meals; a meal is composed of recipes; a recipe is composed of foods; and foods are composed of nutrients.
Modelling the hierarchy
A rigid model of this setup, as used in apps like MyFitnessPal or macro-traco could define each layer separately to consist of a list of items from the layer below. Such a model is inflexible – adding new layers requires substantial work – and even its initial setup is painful. (Five database tables?) And moreover, at a basic level, a straightforward such model would require that meals consist only of recipes, making it annoying to model meals like “chicken broccoli and rice” that are simply composed naturally of foods, living two (instead of one) layers away in the hierarchy.
Instead of choosing a particular number of layers, let’s define all infinitely many layers by induction. Here is an inductive definition of Food. (I’ll use capital-F Food to refer to items generated by this inductive process at any layer as opposed to lowercase-F food for those items described above as having nutrition facts labels, living at layer 1 of the model.)
- Base case. Each Nutrient is a Food.
- Step case. If \(F_1,\ldots,F_n\) are each a Food, then the Compound \(\langle F_1, \ldots, F_n \rangle\) is a Food.
Mathematically speaking, a Food is an \(n\)-ary tree whose leaves are labelled according to what nutrient is represented there.
In this inductive model, nutrition facts is defined by a totally straightfoward recursion on Food, which one normally learns to implement in kindergarten.
- If the Food is a Nutrient, then it is its nutrition facts.
- If the Food is a Compound \(\langle F_1, \ldots, F_n \rangle\), then its nutrition facts is simply the sum of the nutrition facts of each \(F_i\).
How much chicken?
I’ve thus far omitted a crucial aspect of the model: what are the quantities of the Foods that go into forming a Compound? Recall that Compounds are things like meals, which we would naturally express as “two cups of cooked white rice, one large chicken breast, and 200 grams of broccoli.”
In that natural language description, the foods are “cooked white rice”, “chicken breast”, and “broccoli”, but their respective quantities are “two cups”, “one large”, and “200 grams”. Quantities are composed of a number together with a unit. The units here are “cups”, “large,” and “grams”.
In light of this, I define a Quantified Food as a Quantity together with a Food. A Quantity is just a number and a unit. The unit is specific to the associated Food, so a “cup” of “cooked white rice” is distinct from a “cup” of “dry steel-cut oats.”
Now let’s revise the step case of the definition of Food.
- If \(Q_1, \ldots, Q_n\) are each a Quantified Food, then \(\langle Q_1, \ldots, Q_n \rangle\) is a Food.
Mathematically speaking, we still have an \(n\)-ary tree, but the edges in that tree are now labelled with Quantities.
The recursive definition of nutrition facts changes slightly: it suffices to multiply the nutrition facts of each Food in a Compound by its Quantity before summing.
In practice
Nutcalc implements this inductive model of food. It is a domain-specific programming language for defining Foods and performing computations on them.
A Nutcalc program consists of a series of Food definitions.
1 cup 'cooked white rice' weighs 158 g:
- 0.4 g fat + 4.3 g protein + 45 g carbs
- 1.9 mg iron
1 large 'chicken breast' weighs 120 g:
- 4.3 g fat + 37 g protein
- 89 mg sodium + 102 mg cholesterol + 1.2 mg iron + 307 mg potassium
100 g broccoli:
- 0.4 g fat + 7 g carbs + 2.8 g protein
- 33 mg sodium + 316 mg potassium
The effect of executing these statements in the Nutcalc interpreter is to define the Foods ‘cooked white rice’ and ‘chicken breast’ together with the respective units ‘cup’ and ‘large’ whose equivalent weights are given. For broccoli, since the definition is already for a particular weight, there’s no need for a ‘weighs’ clause.
Since Nutcalc uses the inductive model of food, we use the same syntax to define meals. For meals,
the most natural unit is usually ‘serving’, but that’s long to type so how about x
?
1 x 'chicken broccoli rice':
- 1 large 'chicken breast' + 2 cup 'cooked white rice' + 200 g broccoli
When a ‘weighs’ clause is omitted but the definition defines a new unit (here x
), the weight of
the new unit is inferred as the sum of the weights of the constituent Foods.
For meals, not only is that assumption about weight usually correct, but we often don’t care
anyway about the weights of Foods at the higher levels in the model.
Again, we use the same syntax to define a meal plan:
1 x Monday:
- 1 x 'oatmeal breakfast'
- 1 x 'eggs sausage bacon toast lunch'
- 1 x 'chicken broccoli rice'
- 1 x 'protein shake'
Of course, the same syntax is used to define a food journal:
1 x '2025-02-20':
- 0.5 cup 'dry steel-cut oats' + 50 g walnuts + 2 cup '3.25% milk'
- 4 x 'breakfast sausage' + 4 large egg + 2 slice toast + 2 tsp butter
- 1 medium 'chicken break' + 1 medium 'chicken leg' + 2.5 cup 'cooked white rice' + 200 g broccoli
After loading a file with our definitions, we can compute aggregate nutrition facts easily:
$ nutcalc -i journal.nut
nutcalc> facts 1 x 'oatmeal breakfast'
energy: 1122.83 kcal
protein: 44.30 g
fat: 46.57 g
carbs: 131.62 g
water: 150.60 g
calcium: 602.41 mg
iron: 4.35 mg
potassium: 930.49 mg
sodium: 84.34 mg
zinc: 2.71 mg
cholesterol: 30.12 mg
nutcalc>
In fact, facts
accepts an expression on the right, which can be a lone Quantified Food or a sum
thereof, e.g. 2 cup 'cooked white rice' + 200 g broccoli
.
A programming language?
Some might laugh at the idea of calling Nutcalc a “programming language.” It has no loops, no conditions, no mutable variables, no functions, no recursion. It is certainly not Turing-complete.
In my view, Nutcalc is a programming language because it has a syntax and a semantics. Both are very simple, but I see that as a strength of the language, not a weakness.
Overall, whether Nutcalc is or isn’t a programming language is incidental. It’s useful. Anyway, the main contribution is the inductive model of food, giving a unified view of foods, meals, and so on.