clojureconstraint-programminglogic-programmingclojure-core.logicminikanren

Modelling recipes and available ingredients with constraint logic


Imagine I have a number of recipes for different dishes and a larder containing various ingredients in my kitchen. I want to construct a model using core.logic which will enable me to answer the following question: for a given set of ingredients (i.e. those in my larder now) which recipes can I make?

The recipes are somewhat flexible and I need to be able to model this. Later on I'd like to add quantities to them, but let's leave this out for the moment in order to get started.

I can see how to model the larder:

(db-rel in-larder x)
(def larder (db
             [in-larder :carrots]
             [in-larder :rice]
             [in-larder :garlic]))

The recipes have a name and a list of ingredients which may be optional or combine in various ways. There are n recipes. As an example recipes may look (informally) like this:

Risotto A
=========
(carrots OR peas)
rice
(onions OR garlic)

Risotto B
=========
((carrots AND onions)) OR (rice AND peas))
garlic

I am grappling with how to express this in core.logic. (N.B. the text above is just illustrative and is not intended to be machine readable.)

I imagine the query would look something like this:

(with-dbs [larder recipes] (run* [q] (possible-recipe q)))

which would return the following result (given the larder definition above):

(:risotto-a :risotto-b)

My question is this: how can I model these recipes so that I can write a query over the recipes and the larder to list the names of the possible recipes given the current contents of my larder?


Solution

  • Here's one way to model this problem:

    (db-rel in-larder i)
    (db-rel recipe r)
    (db-rel in-recipe r i)
    (db-rel compound-ingredient i is)
    
    (def recipes (db
                   [compound-ingredient :carrots-or-peas [:or :carrots :peas]]
                   [compound-ingredient :onions-or-garlic [:or :onions :garlic]]
                   [compound-ingredient :carrots-and-onions [:and :carrots :onions]]
                   [compound-ingredient :rice-and-peas [:and :rice :peas]]
                   [compound-ingredient :carrots-onions-or-rice-peas [:or :carrots-and-onions :rice-and-peas]]  
                   [recipe :risotto-a]
                   [recipe :risotto-b]
                   [in-recipe :risotto-a [:carrots-or-peas :rice :onions-or-garlic]]
                   [in-recipe :risotto-b [:garlic :carrots-onions-or-rice-peas]]))
    
    (defn possible-recipe [r]
      (recipe r)
      (fresh [ingredients]
             (in-recipe r ingredients)
             (all-ingredients-in-lardero ingredients)))
    

    There are recipes and a list of ingredients for each recipe. Each ingredient can be single or compound, in which case it can have optional or mandatory ingredients.

    We need some more relations to make it work:

    (defne any-ingredient-in-lardero [ingredients]
      ([[?i . ?morei]] (conda [(ingredient-in-lardero ?i)]
                              [(emptyo ?morei) fail]
                              [(any-ingredient-in-lardero ?morei)])))
    
    (defne all-ingredients-in-lardero [ingredients]
      ([[?i . ?morei]]
       (ingredient-in-lardero ?i)
       (conda [(emptyo ?morei)]
              [(all-ingredients-in-lardero ?morei)])))
    
    (defn ingredient-in-lardero [i]
      (conde        
        [(fresh [composition op sub-ingredients]
                (compound-ingredient i composition)
                (conso op sub-ingredients composition)
    
                (conde
                  [(== :or op) (any-ingredient-in-lardero sub-ingredients)]
                  [(== :and op) (all-ingredients-in-lardero sub-ingredients)]))]
        [(in-larder i)]))
    

    Now we can query with different larders to get the recipes:

    (def larder-1 (db [in-larder :carrots] [in-larder :rice] [in-larder :garlic])) 
    (def larder-2 (db [in-larder :peas]    [in-larder :rice] [in-larder :garlic]))
    
    (with-dbs [recipes larder-1]
      (run* [q]
            (possible-recipe q)))
    ;=> (:risotto-a)
    
    (with-dbs [recipes larder-2]
      (run* [q]
            (possible-recipe q)))
    ;=> (:risotto-a :risotto-b)
    

    Full code is in this gist