I used to love solving logic puzzles as a kid. Bought books of 'em. Great fun. They're a puzzle with this sort of shape:
It cries out, "Logic Programming", doesn't it? Well, recently the London Cloure group took a crack at it. We got pretty far, but didn't complete it before the night was over. I thought I'd finish it off and write it up, for your pleasure. Here goes...
I've taken a puzzle from Logic Puzzles.org, who seem to have a army of them. Here are the sets of things we're dealing with:
And here are the rules. You can skim over them. We'll go through them in detail in part two.
- Of Landon and Jason, one has the 7:30pm reservation and the other loves mozzarella.
- The blue cheese enthusiast subscribed to Fortune.
- The muenster enthusiast didn't subscribe to Vogue.
- The 5 people were the Fortune subscriber, Landon, the person with a reservation at 5:00pm, the mascarpone enthusiast, and the Vogue subscriber.
- The person with a reservation at 5:00pm didn't subscribe to Time.
- The Cosmopolitan subscriber has an earlier reservation than the mascarpone enthusiast.
- Bailey has a later reservation than the blue cheese enthusiast.
- Either the person with a reservation at 7:00pm or the person with a reservation at 7:30pm subscribed to Fortune.
- Landon has a later reservation than the Time subscriber.
- The Fortune subscriber is not Jamari.
- The person with a reservation at 5:00pm loves mozzarella.
There's only one possible solution, and core.logic
is going to find it for us.
lvar
?Imagine a closed box. I'll tell you that inside the box is
something that is grey, weighs one tonne, and has a horn on its
nose. When we open the box, you won't be surprised to see it
contains a rhino. That's how I think of a logic variable, or
lvar
. It's a box containing something that's unknown right
now. We can state some rules about what must be inside, and when we
come to open the box, the right kind of thing will pop out.
So, with that in mind, let's set up our solution.
We'll start with this code:
(let [people (repeatedly 5 lvar)
magazines (repeatedly 5 lvar)
cheeses (repeatedly 5 lvar)
reservations (repeatedly 5 lvar)
answers (map list people magazines cheeses reservations)]
(run* [q]
(== q answers)))
Here, we've done four things:
lvar
function five times, to create boxes for the five people.(map list ...)
, to
create a structure that's a list of five `[person, magazine, cheese,reservation-time]tuples. When we open all those boxes, we'll have
our answer.
4.
run` a simple query: "Find all the qs, such that q is a possible answer."
So far, so good. Now all we need to do is add the right constraints, and the boxes will magically open to reveal the single answer to the puzzle.
So the first thing we need to constrain is the list of people, magazines, cheeses and reservations. This would be a start:
(let [people (repeatedly 5 lvar)
magazines (repeatedly 5 lvar)
cheeses (repeatedly 5 lvar)
reservations (repeatedly 5 lvar)
answers (map list people magazines cheeses reservations)]
(run 1 [q]
(== q answers)
(permuteo people [:amaya :bailey :jamari :jason :landon])
(permuteo magazines [:fortune :time :cosmopolitan :us-weekly :vogue])
(permuteo cheeses [:asiago :blue-cheese :mascarpone :mozzarella :muenster])
(permuteo reservations [5 6 7 7.5 8.5])))
That says that people
matches the list of five names, but not
necessarily in the same order. It's a permutation of the given
list. We repeat that pattern for all four kinds of thing.
That's almost right, but there's a problem with ordering. Even once we've figured out the correct tuples, we're going to get them in all possible orders, which is 5! or 120 answers, all of which are equivalent.
There are a few ways to handle this. We could to use a Set for the
answers, instead of a list. That would be good in theory, but
core.logic
doesn't support setsyet. We could ignore the
problem and just take whichever answer pops out first. Or we could
define a fixed order for any one of the collections, and let the
others fall in line with that.
Fixing the order is the approach I like, because then there's only one possible solution, which makes it easy to check we've got the rest of the code right later. So let's fix the order of the people:
(let [people (repeatedly 5 lvar)
magazines (repeatedly 5 lvar)
cheeses (repeatedly 5 lvar)
reservations (repeatedly 5 lvar)
answers (map list people magazines cheeses reservations)]
(run 1 [q]
(== q answers)
(== people [:amaya :bailey :jamari :jason :landon])
(permuteo magazines [:fortune :time :cosmopolitan :us-weekly :vogue])
(permuteo cheeses [:asiago :blue-cheese :mascarpone :mozzarella :muenster])
(permuteo reservations [5 6 7 7.5 8.5])))
And with that, we're ready to start encoding puzzle rules. We'll pull them out as separate functions, so our final main function will look like this:
(let [people (repeatedly 5 lvar)
magazines (repeatedly 5 lvar)
cheeses (repeatedly 5 lvar)
reservations (repeatedly 5 lvar)
answers (map list people magazines cheeses reservations)]
(run 1 [q]
(== q answers)
(== people [:amaya :bailey :jamari :jason :landon])
(rule-1 answers)
(rule-2 answers)
(rule-3 answers)
(rule-4 answers)
(rule-5 answers)
(rule-6 answers)
(rule-7 answers)
(rule-8 answers)
(rule-9 answers)
(rule-10 answers)
(rule-11 answers)
(permuteo magazines [:fortune :time :cosmopolitan :us-weekly :vogue])
(permuteo cheeses [:asiago :blue-cheese :mascarpone :mozzarella :muenster])
(permuteo reservations [5 6 7 7.5 8.5])))
We'll look at the individual rules in part two...