16

I've just started playing with core.logic, and to work on it I'm trying to implement something simple that is similar to a problem that I am currently working on professionally. However, one part of the problem has me stumped...

As a simplification of my example, if I have a catalog of items, and some of them are only available in certain countries, and some are not available in specific countries. I'd like to be able specify the list of items, and the exceptions, something like:

(defrel items Name Color)
(defrel restricted-to Country Name)
(defrel not-allowed-in Country Name)

(facts items [['Purse 'Blue]
              ['Car 'Red]
              ['Banana 'Yellow]])

(facts restricted-to [['US 'Car]])

(facts not-allowed-in [['UK 'Banana]
                       ['France 'Purse]])

If possible, I'd rather not specify allowed-in for all countries, as the set of items with restrictions is relatively small, and I'd like to be able to make a single change to allow/exclude for an item for a given country.

How can I write a rule that gives the list of items/colors for a country, with the following constraints:

  • The item must be in the list of items
  • The country/item must be not be in the 'not-allowed-in' list
  • Either:
    • There is no country in the restricted-to list for that item
    • The country/item pair is in the restricted-to list

Is there some way to do this? Am I thinking about things in entirely the wrong way?

kolen
  • 2,282
  • 2
  • 26
  • 32
Peter Hart
  • 4,745
  • 2
  • 20
  • 28

2 Answers2

14

Usually when you start negating goals in logic programming, you need to reach for non-relational operations (cut in Prolog, conda in core.logic).

This solution should only be called with ground arguments.

(defn get-items-colors-for-country [country]
  (run* [q]
    (fresh [item-name item-color not-country]
      (== q [item-name item-color])
      (items item-name item-color)
      (!= country not-country)

      (conda
        [(restricted-to country item-name)
         (conda
           [(not-allowed-in country item-name)
            fail]
           [succeed])]
        [(restricted-to not-country item-name)
         fail]
        ;; No entry in restricted-to for item-name
        [(not-allowed-in country item-name)
         fail]
        [succeed]))))

(get-items-colors-for-country 'US)
;=> ([Purse Blue] [Banana Yellow] [Car Red])

(get-items-colors-for-country 'UK)
;=> ([Purse Blue])

(get-items-colors-for-country 'France)
;=> ([Banana Yellow])

(get-items-colors-for-country 'Australia)
;=> ([Purse Blue] [Banana Yellow])

Full solution

Ambrose
  • 1,210
  • 12
  • 18
  • By 'ground arguments', I assume you mean a value, rather than query variables? Apologies, my last brush with logic programming was an undergrad prolog course almost 25 years ago... – Peter Hart Jan 03 '12 at 22:31
  • 1
    It cannot be an unbound or unground variable. A value is ground if it does not contain unbound logic variables (eg. [1 2 0._] is not ground). This becomes relevant if the function is a goal and you're passing query variables as arguments. In this gist, `items-colors-for-country` requires its first argument to be ground. The goal is more flexible and composable than my initial answer. For example we can query colors of Purses available in US. https://gist.github.com/1557417 – Ambrose Jan 03 '12 at 23:14
  • @PeterHart I'm assuming the professional project that this is based around is database based? If you're interested in extending core.logic to query this database directly, I've dumped some material [here](https://github.com/frenchy64/Logic-Starter/blob/master/src/logic_introduction/extend.clj) – Ambrose Jan 05 '12 at 04:45
  • actually it's all done in memory using collections (java code) and/or xpath queries. The actual problem is much more complex - it involves overlapping hierarchies. UNfortunately the chance of me getting clojure into the environment are fairly slim. I'll take a look at your example, though, as I'm trying to understand this so that if I ever get the chance to make a case, I'll be knowledgeable enough to do a good job. – Peter Hart Jan 05 '12 at 14:50
2

Conda may complexifies the code, using nafc, you can more easily reorder goals if you want. This is still non-relational ! :)

(ns somenamespace
  (:refer-clojure :exclude [==])
  (:use [clojure.core.logic][clojure.core.logic.pldb]))

(db-rel items Name Color)
(db-rel restricted-to Country Name)
(db-rel not-allowed-in Country Name)

(def stackoverflow-db 
  (db [items 'Purse 'Blue]
      [items  'Car 'Red]
      [items 'Banana 'Yellow]
      [restricted-to 'US 'Car]
      [not-allowed-in 'UK 'Banana]
      [not-allowed-in 'France 'Purse]))


(defn get-items-colors-for-country [country]
  (with-db stackoverflow-db
    (run* [it co]
         (items  it co)
         (nafc not-allowed-in country it)
         (conde 
          [(restricted-to country it)]
          [(nafc #(fresh [not-c] (restricted-to not-c %)) it)]))))

(get-items-colors-for-country 'US)
;=> ([Purse Blue] [Banana Yellow] [Car Red])

(get-items-colors-for-country 'UK)
;=> ([Purse Blue])

(get-items-colors-for-country 'France)
;=> ([Banana Yellow])

(get-items-colors-for-country 'Australia)
;=> ([Purse Blue] [Banana Yellow])

For more examples : https://gist.github.com/ahoy-jon/cd0f025276234de464d5

jwinandy
  • 1,713
  • 12
  • 19