Visar inlägg med etikett Python. Visa alla inlägg
Visar inlägg med etikett Python. Visa alla inlägg

2008-10-29

If the only tool you have...

"If the only tool you have is a hammer, you will see every problem as a nail." - Abraham Maslow

This week, I've been experimenting with Monte Carlo methods for a Pentago AI player. I've written pattern-based AI players for Pentago before and (somewhere) I've got an unfinished implementation that uses Minimax as well.

I didn't really expect much from this little experimental player (it's only about 100 lines of Python code) but it has turned out to be "not too bad". Despite the fact that it only considers one game state at a time... maybe that says more about my other players though ;-)

I'll try to add MCTS (Monte Carlo Tree Search) during this week, maybe even UCT (Upper Confidence bounds applied to Trees) which ought to make the player quite a bit stronger.

2008-10-19

Pattern matching function declarations in Python part II

Ok, so the pattern matching idea kind of backfired.

I was under the impression that a function's default arguments wouldn't be evaluated until the function was called. Which would have meant that I could've changed them, into valid Python code, in the decorator function. But... that's not the way it works.

My first attempt at something more interesting than matching a constant failed miserably. I wanted to use a variable to constrain that matching process. For example:

>>> @patternmatched
... def foo(lst = [__, var, var, __]):
... pass
should match any lst which is a list and where there is, at least one, repeated element somewhere in it. Both [1,1,2,3,4] and [3,2,1,1,2] should match, whilst [1,2,1,3,1] shouldn't.

Unfortunately. Trying the above results in a NameError since both __ and var are unbound. And, seeing that, I remembered that this is *exactly* what happened about three years ago when I was trying out various syntaxes for PyRete. It's the reason I decided to put all of the matching in the if-statement instead of in the function arguments.

But, it's not quite time yet to give up. There are several possible ways to go about this. The "best" choice is probably to use Andrew's python4ply, or maybe PyPy. Either way, I'll get a chance to compare and contrast the work of implementing a DSL in Python versus Common Lisp.

2008-10-13

Pattern matching function declarations in Python

All those who have tried to write a rules-based program know that there are, at least, two differences between "regular" programming and rules-based programming. The first is that you have no direct control over the flow of execution and the second is that, instead of specifying input parameters, you specify patterns that should be matched for a certain "function" (or rule) to be invoked.

The bit about giving up control is quite difficult for most programmers (at least the ones I know) and it doesn't translate well to other types of programming anyway. But it would be interesting to try and add pattern matching functions to a language such as Python and see whether it could be useful (or at least fun to experiment with).

It's not a new idea. The first language to support pattern matching this way was Snobol (version 4 I think) which was introduced in the late 60s/early 70s. Both Haskell and Erlang has it and there are a bunch of others as well, Qi and Prolog to mention a few.

I'm thinking about something along the lines of:

>>> from patternmatching import *
>>> @patternmatched
... def fac(n = 0):
... return 1
...
>>> @patternmatched
... def fac(n = int):
... return n* fac(n = n-1)
...
>>> fac(5)
120
>>>
The idea is that the first function would handle any calls to the fac function where the parameter n is 0 and the second function would handle any calls where the parameter n is an int (but not 0). And, yes. I've got the above working already. The tricky bits are providing more elaborate forms of pattern matching with the available syntax.

I'll be back shortly with some code...

2008-09-02

Roman numerals

Yesterday we had another GothPy meeting. Apart from getting some formalities out of the way, Emily showed us how to do the FizzBuzz kata in just over 4 minutes. Quite impressive. This was one of the things she and Michael Feathers had done for the Programming with the Stars competition at Agile 2008.

We were also supposed to tackle a "new" kata, converting numbers to roman numerals, but a debate about what rules should be used to guide the conversion (for example, should 1999 be 'IMM' or 'MCMXCIX'?) got us into doing an english translation instead (converting numbers to english text). And as the smug Lisp weenie I have turned into I couldn't help but to inform everyone that the built-in format directives ~@R and ~R solves those problems quite neatly. The hard part is of course to write the Lisp implementation ;-)

Anyway, as usual, it turned out we thought about the problem a bit differently but we quickly converged around a "good enough" strategy even though it wasn't particularly pretty and we ended up with lots of code duplication. I learned a lot though. It put my previous solution in a different light and it's always interesting to see and hear how others tackle awkward problems like this.

And BTW. Here is my attempt at the Roman numerals kata (using doctest):

def as_roman_numeral(num):
"""
Returns a string containing the roman numeral representation of num or
None if num is outside of range(1,4000).

>>> as_roman_numeral(4000) == None
True
>>> as_roman_numeral(1)
'I'
>>> as_roman_numeral(99)
'XCIX'
>>> as_roman_numeral(888)
'DCCCLXXXVIII'
>>> as_roman_numeral(1999)
'MCMXCIX'
>>> as_roman_numeral(2001)
'MMI'
"""

units = [0, 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX']
tens = [0, 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC']
hundreds = [0, 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM']
thousands = [0, 'M', 'MM', 'MMM']

if num in xrange(1,4000):
th, mils = num//1000, num%1000
h, cents = mils//100, mils%100
te, u = cents//10, cents%10
return "".join(numerals[digit] for digit, numerals in [(th, thousands),
(h, hundreds),
(te, tens),
(u, units)] if digit > 0)
else:
return None
It works with numbers in range(1,4000) and it implements the rules as described on Wikipedia.

2008-07-18

Sling Blade Runner IV

I've been doing some more work on my kōan, Sling Blade Runner. I've given up on the beam-stack search approach. I doubt it will improve results much. If any at all.

Instead I have spent some time on improving the extend method so that it can find more extensions. And it is becoming quite efficient in exploring the neighbourhood of any given pair of titles. Previously it was only looking for extensions between two adjacent titles in the chain but now it searches for extensions between end-points in a whole sequence of titles instead. Consider the chain [A,B,C,D,...]. Now, if there's an extension between A and C, other than, and that is longer than [B], the chain is extended with it.

This, simple, tweak makes the search slower (of course) but it also improves the lengths of almost all chains that are explored. It rarely ends up with a chain that is shorter than 235 titles and most end up longer than 250 (246 was the previous best) titles.

The longest chain found is now 265 titles:

1 THE GOSPEL
2 THE GOSPEL OF JOHN
3 JOHN Q
4 Q AND A
5 A RIVER RUNS THROUGH IT
6 IT HAPPENED AT THE WORLDS FAIR
7 FAIR GAME
8 GAME OF DEATH
9 DEATH BECOMES HER
10 HER MAJESTY MRS BROWN
11 BROWN SUGAR
12 SUGAR AND SPICE
13 SPICE WORLD
14 WORLD TRADE CENTER
15 CENTER STAGE
16 STAGE FRIGHT
17 FRIGHT NIGHT
18 NIGHT OF THE LIVING DEAD
19 DEAD BANG
20 BANG BANG YOURE DEAD
21 DEAD MAN WALKING
22 WALKING AND TALKING
23 TALKING ABOUT SEX
24 SEX AND THE OTHER MAN
25 MAN OF THE HOUSE
26 HOUSE OF DRACULA
27 DRACULA DEAD AND LOVING IT
28 IT HAPPENED ONE NIGHT
29 ONE NIGHT STAND
30 STAND IN
31 IN GODS HANDS
32 HANDS ON A HARD BODY
33 BODY AND SOUL
34 SOUL FOOD
35 FOOD OF LOVE
36 LOVE IS THE DEVIL
37 THE DEVIL RIDES OUT
38 OUT COLD
39 COLD FEVER
40 FEVER PITCH
41 PITCH BLACK
42 BLACK LIKE ME
43 ME WITHOUT YOU
44 YOU LIGHT UP MY LIFE
45 MY LIFE SO FAR
46 FAR FROM HOME THE ADVENTURES OF YELLOW DOG
47 DOG RUN
48 RUN SILENT RUN DEEP
49 DEEP BLUE
50 DEEP BLUE SEA
51 SEA OF LOVE
52 LOVE WALKED IN
53 IN OLD CALIFORNIA
54 CALIFORNIA SPLIT
55 SPLIT SECOND
56 SECOND BEST
57 BEST MEN
58 MEN WITH GUNS
59 GUNS OF THE MAGNIFICENT SEVEN
60 THE MAGNIFICENT SEVEN
61 SEVEN YEARS IN TIBET
62 TIBET CRY OF THE SNOW LION
63 LION OF THE DESERT
64 DESERT HEARTS
65 HEARTS OF DARKNESS A FILMMAKERS APOCALYPSE
66 APOCALYPSE NOW
67 NOW YOU SEE HIM NOW YOU DONT
68 DONT BOTHER TO KNOCK
69 KNOCK OFF
70 OFF THE BLACK
71 THE BLACK ANGEL
72 ANGEL EYES
73 EYES OF AN ANGEL
74 ANGEL BABY
75 BABY SECRET OF THE LOST LEGEND
76 LEGEND OF THE LOST
77 THE LOST BOYS
78 BOYS LIFE
79 LIFE OR SOMETHING LIKE IT
80 IT TAKES TWO
81 TWO MEN WENT TO WAR
82 WAR OF THE WORLDS
83 THE WORLDS FASTEST INDIAN
84 INDIAN SUMMER
85 SUMMER CATCH
86 CATCH ME IF YOU CAN
87 YOU CAN COUNT ON ME
88 ME MYSELF I
89 I WANT TO LIVE
90 LIVE FOREVER
91 FOREVER YOUNG
92 YOUNG SHERLOCK HOLMES
93 SHERLOCK HOLMES IN WASHINGTON
94 WASHINGTON SQUARE
95 SQUARE DANCE
96 DANCE WITH A STRANGER
97 STRANGER IN THE HOUSE
98 THE HOUSE OF THE DEAD
99 DEAD OF NIGHT
100 NIGHT AND THE CITY
101 THE CITY
102 CITY OF ANGELS
103 ANGELS WITH DIRTY FACES
104 FACES OF DEATH
105 DEATH WISH
106 WISH UPON A STAR
107 A STAR IS BORN
108 BORN TO BE WILD
109 WILD THINGS
110 THINGS TO COME
111 COME AND GET IT
112 IT RUNS IN THE FAMILY
113 THE FAMILY MAN
114 MAN ON FIRE
115 FIRE ON THE MOUNTAIN
116 THE MOUNTAIN MEN
117 MEN IN BLACK
118 BLACK HAWK DOWN
119 DOWN WITH LOVE
120 LOVE AND DEATH
121 DEATH WISH V THE FACE OF DEATH
122 DEATH SHIP
123 SHIP OF FOOLS
124 FOOLS RUSH IN
125 IN THE WINTER DARK
126 DARK STAR
127 STAR TREK IV THE VOYAGE HOME
128 HOME ALONE
129 ALONE IN THE DARK
130 DARK BLUE
131 BLUE CAR
132 CAR 54 WHERE ARE YOU
133 YOU CANT TAKE IT WITH YOU
134 YOU ONLY LIVE ONCE
135 ONCE IN THE LIFE
136 LIFE AS A HOUSE
137 HOUSE PARTY
138 HOUSE PARTY 3
139 3 NINJAS
140 3 NINJAS KNUCKLE UP
141 UP CLOSE AND PERSONAL
142 PERSONAL BEST
143 BEST OF THE BEST
144 BEST OF THE BEST 3 NO TURNING BACK
145 NO TURNING BACK
146 BACK TO SCHOOL
147 SCHOOL OF ROCK
148 ROCK STAR
149 STAR TREK THE MOTION PICTURE
150 PICTURE BRIDE
151 BRIDE OF THE MONSTER
152 MONSTER HOUSE
153 HOUSE OF FRANKENSTEIN
154 FRANKENSTEIN AND THE MONSTER FROM HELL
155 FROM HELL
156 HELL UP IN HARLEM
157 HARLEM RIVER DRIVE
158 DRIVE ME CRAZY
159 CRAZY AS HELL
160 HELL NIGHT
161 NIGHT FALLS ON MANHATTAN
162 MANHATTAN MURDER MYSTERY
163 MYSTERY ALASKA
164 ALASKA SPIRIT OF THE WILD
165 THE WILD
166 THE WILD ONE
167 ONE NIGHT WITH THE KING
168 THE KING AND I
169 I SPY
170 SPY HARD
171 HARD EIGHT
172 EIGHT MEN OUT
173 OUT OF THE BLUE
174 BLUE SKY
175 SKY HIGH
176 HIGH CRIMES
177 CRIMES OF PASSION
178 PASSION IN THE DESERT
179 DESERT BLUE
180 BLUE STEEL
181 STEEL DAWN
182 DAWN OF THE DEAD
183 THE DEAD
184 DEAD MAN ON CAMPUS
185 CAMPUS MAN
186 MAN TROUBLE
187 TROUBLE EVERY DAY
188 DAY OF THE WOMAN
189 THE WOMAN IN RED
190 RED EYE
191 EYE FOR AN EYE
192 EYE OF GOD
193 GOD TOLD ME TO
194 TO DIE FOR
195 FOR THE BOYS
196 THE BOYS
197 BOYS AND GIRLS
198 GIRLS WILL BE GIRLS
199 GIRLS GIRLS GIRLS
200 GIRLS OF SUMMER
201 SUMMER LOVERS
202 LOVERS AND OTHER STRANGERS
203 STRANGERS WHEN WE MEET
204 MEET JOE BLACK
205 BLACK AND WHITE
206 WHITE HUNTER BLACK HEART
207 HEART CONDITION
208 CONDITION RED
209 RED RIVER
210 RIVER OF NO RETURN
211 RETURN OF THE FLY
212 FLY AWAY HOME
213 HOME ALONE 2 LOST IN NEW YORK
214 NEW YORK NEW YORK
215 NEW YORK COP
216 COP LAND
217 LAND OF THE DEAD
218 DEAD END
219 END OF DAYS
220 DAYS OF HEAVEN
221 HEAVEN CAN WAIT
222 WAIT UNTIL DARK
223 DARK CITY
224 CITY OF JOY
225 JOY RIDE
226 RIDE THE HIGH COUNTRY
227 COUNTRY LIFE
228 LIFE WITH FATHER
229 FATHER OF THE BRIDE
230 BRIDE OF THE WIND
231 THE WIND AND THE LION
232 THE LION KING
233 KING OF THE JUNGLE
234 THE JUNGLE BOOK
235 JUNGLE BOOK
236 BOOK OF LOVE
237 LOVE LIFE
238 LIFE IS BEAUTIFUL
239 BEAUTIFUL PEOPLE
240 PEOPLE I KNOW
241 I KNOW WHERE IM GOING
242 IM GOING HOME
243 HOME ALONE 3
244 3 NINJAS KICK BACK
245 BACK TO THE BEACH
246 BEACH PARTY
247 PARTY MONSTER
248 MONSTER IN A BOX
249 BOX OF MOON LIGHT
250 LIGHT OF DAY
251 DAY FOR NIGHT
252 NIGHT MOTHER
253 MOTHER NIGHT
254 NIGHT ON EARTH
255 EARTH GIRLS ARE EASY
256 EASY MONEY
257 MONEY FOR NOTHING
258 NOTHING BUT TROUBLE
259 TROUBLE IN PARADISE
260 PARADISE ROAD
261 ROAD HOUSE
262 HOUSE PARTY 2
263 2 DAYS IN THE VALLEY
264 VALLEY GIRL
265 GIRL WITH A PEARL EARRING
The results vary a bit depending on search depth (not that surprising really) so there may still be some possible improvements by just modifying parameters.

[Update 2008-07-20] The longest chain found is now 268 titles long.

2008-07-02

Sling Blade Runner III

Continuing work on my Sling Blade Runner solution has led me to improve the search algorithm a bit further. Instead of looking for good starting-points for a search, it now looks for good mid-points and searches both forwards and backwards. This should, in theory, be a better way to do it and it does improve the average length of chains. But it turns out that it doesn't make *that* big a difference on the longest one (roughly 10 titles). The longest chain found is now 246 titles long.

1 THE PRICE OF MILK
2 MILK AND HONEY
3 HONEY I BLEW UP THE KID
4 THE KID
5 THE KID AND I
6 I WENT DOWN
7 DOWN TO YOU
8 YOU ONLY LIVE ONCE
9 ONCE AROUND
10 AROUND THE BEND
11 BEND OF THE RIVER
12 THE RIVER
13 THE RIVER WILD
14 WILD THINGS
15 THINGS TO COME
16 COME AND GET IT
17 IT TAKES TWO
18 TWO EVIL EYES
19 EYES OF AN ANGEL
20 ANGEL HEART
21 HEART CONDITION
22 CONDITION RED
23 RED RIVER
24 RIVER OF NO RETURN
25 RETURN TO HORROR HIGH
26 HIGH SCHOOL HIGH
27 HIGH CRIMES
28 CRIMES OF PASSION
29 PASSION IN THE DESERT
30 DESERT HEARTS
31 HEARTS OF DARKNESS A FILMMAKERS APOCALYPSE
32 APOCALYPSE NOW
33 NOW YOU SEE HIM NOW YOU DONT
34 DONT BOTHER TO KNOCK
35 KNOCK OFF
36 OFF THE BLACK
37 THE BLACK ANGEL
38 ANGEL BABY
39 BABY SECRET OF THE LOST LEGEND
40 LEGEND OF THE LOST
41 THE LOST BOYS
42 BOYS AND GIRLS
43 GIRLS JUST WANT TO HAVE FUN
44 FUN AND FANCY FREE
45 FREE WILLY
46 FREE WILLY 2 THE ADVENTURE HOME
47 HOME ALONE 3
48 3 NINJAS
49 3 NINJAS KNUCKLE UP
50 UP CLOSE AND PERSONAL
51 PERSONAL BEST
52 BEST OF THE BEST
53 BEST OF THE BEST 3 NO TURNING BACK
54 NO TURNING BACK
55 BACK STAGE
56 STAGE FRIGHT
57 FRIGHT NIGHT
58 NIGHT MOTHER
59 MOTHER NIGHT
60 NIGHT FALLS ON MANHATTAN
61 MANHATTAN MURDER MYSTERY
62 MYSTERY ALASKA
63 ALASKA SPIRIT OF THE WILD
64 THE WILD
65 THE WILD ONE
66 ONE NIGHT WITH THE KING
67 THE KING AND I
68 I NEVER PROMISED YOU A ROSE GARDEN
69 GARDEN STATE
70 STATE FAIR
71 FAIR GAME
72 GAME OF DEATH
73 DEATH WISH V THE FACE OF DEATH
74 DEATH SHIP
75 SHIP OF FOOLS
76 FOOLS RUSH IN
77 IN PRAISE OF OLDER WOMEN
78 WOMEN IN LOVE
79 IN LOVE AND WAR
80 WAR OF THE WORLDS
81 THE WORLDS FASTEST INDIAN
82 INDIAN SUMMER
83 SUMMER CATCH
84 CATCH A FIRE
85 FIRE ON THE MOUNTAIN
86 THE MOUNTAIN MEN
87 MEN WITH GUNS
88 GUNS OF THE MAGNIFICENT SEVEN
89 THE MAGNIFICENT SEVEN RIDE
90 RIDE WITH THE DEVIL
91 THE DEVIL RIDES OUT
92 OUT COLD
93 COLD FEVER
94 FEVER PITCH
95 PITCH BLACK
96 BLACK AND WHITE
97 WHITE WATER SUMMER
98 SUMMER LOVERS
99 LOVERS AND OTHER STRANGERS
100 STRANGERS WHEN WE MEET
101 MEET JOE BLACK
102 BLACK HAWK DOWN
103 DOWN WITH LOVE
104 LOVE LIFE
105 LIFE IS BEAUTIFUL
106 BEAUTIFUL GIRLS
107 GIRLS GIRLS GIRLS
108 GIRLS WILL BE GIRLS
109 GIRLS TOWN
110 TOWN AND COUNTRY
111 COUNTRY LIFE
112 LIFE WITH FATHER
113 FATHER OF THE BRIDE
114 BRIDE OF THE WIND
115 THE WIND AND THE LION
116 THE LION KING
117 KING OF THE JUNGLE
118 THE JUNGLE BOOK
119 JUNGLE BOOK
120 BOOK OF LOVE
121 LOVE CRAZY
122 CRAZY AS HELL
123 HELL NIGHT
124 NIGHT ON EARTH
125 EARTH GIRLS ARE EASY
126 EASY MONEY
127 MONEY FOR NOTHING
128 NOTHING BUT TROUBLE
129 TROUBLE EVERY DAY
130 DAY FOR NIGHT
131 NIGHT OF THE LIVING DEAD
132 DEAD MAN
133 DEAD MAN WALKING
134 WALKING AND TALKING
135 TALKING ABOUT SEX
136 SEX AND THE OTHER MAN
137 MAN OF THE HOUSE
138 THE HOUSE OF THE DEAD
139 DEAD END
140 END OF DAYS
141 DAYS OF HEAVEN
142 HEAVEN CAN WAIT
143 WAIT UNTIL DARK
144 DARK STAR
145 STAR WARS EPISODE V THE EMPIRE STRIKES BACK
146 BACK TO THE BEACH
147 THE BEACH
148 BEACH PARTY
149 PARTY MONSTER
150 MONSTER HOUSE
151 HOUSE OF DRACULA
152 DRACULA DEAD AND LOVING IT
153 IT HAD TO BE YOU
154 YOU LIGHT UP MY LIFE
155 MY LIFE
156 LIFE OR SOMETHING LIKE IT
157 IT HAPPENED ONE NIGHT
158 ONE NIGHT STAND
159 STAND IN
160 IN THE HEAT OF THE NIGHT
161 NIGHT AND THE CITY
162 THE CITY
163 CITY BY THE SEA
164 SEA OF LOVE
165 LOVE WALKED IN
166 IN THE COMPANY OF MEN
167 MEN IN BLACK
168 BLACK LIKE ME
169 ME MYSELF I
170 I WANT TO LIVE
171 LIVE AND LET DIE
172 DIE MONSTER DIE
173 DIE MOMMIE DIE
174 DIE ANOTHER DAY
175 DAY OF THE DEAD
176 THE DEAD
177 DEAD OF NIGHT
178 NIGHT AND DAY
179 DAY OF THE WOMAN
180 THE WOMAN IN RED
181 RED EYE
182 EYE FOR AN EYE
183 AN EYE FOR AN EYE
184 EYE OF GOD
185 GOD TOLD ME TO
186 TO DIE FOR
187 FOR THE BOYS
188 THE BOYS
189 BOYS LIFE
190 LIFE AS A HOUSE
191 HOUSE OF FRANKENSTEIN
192 FRANKENSTEIN MEETS THE WOLF MAN
193 THE WOLF MAN
194 MAN TROUBLE
195 TROUBLE IN PARADISE
196 PARADISE ROAD
197 ROAD HOUSE
198 HOUSE PARTY
199 HOUSE PARTY 3
200 3 NINJAS KICK BACK
201 BACK TO SCHOOL
202 SCHOOL OF ROCK
203 ROCK STAR
204 STAR TREK IV THE VOYAGE HOME
205 HOME ALONE
206 ALONE IN THE DARK
207 DARK BLUE
208 BLUE CAR
209 CAR 54 WHERE ARE YOU
210 YOU CANT TAKE IT WITH YOU
211 YOU CAN COUNT ON ME
212 ME WITHOUT YOU
213 YOU SO CRAZY
214 CRAZY PEOPLE
215 PEOPLE WILL TALK
216 TALK OF ANGELS
217 ANGELS WITH DIRTY FACES
218 FACES OF DEATH
219 DEATH WISH
220 WISH UPON A STAR
221 A STAR IS BORN
222 BORN AMERICAN
223 AMERICAN DREAM
224 DREAM A LITTLE DREAM
225 DREAM MAN
226 MAN ON FIRE
227 FIRE IN THE SKY
228 SKY HIGH
229 HIGH SPIRITS
230 SPIRITS OF THE DEAD
231 DEAD BANG
232 BANG BANG YOURE DEAD
233 DEAD HEAT
234 HEAT AND DUST
235 DUST TO GLORY
236 GLORY ROAD
237 ROAD GAMES
238 GAMES PEOPLE PLAY NEW YORK
239 NEW YORK NEW YORK
240 NEW YORK COP
241 COP LAND
242 LAND OF THE DEAD
243 DEAD MAN ON CAMPUS
244 CAMPUS MAN
245 MAN ON THE MOON
246 MOON OVER PARADOR
Time: 00:49:32
This is still just a simple best-first search and I still have high hopes for a beam search beam-stack search to do a lot better.

[Correction 2008-07-02]: When I say beam search I mean beam-stack search.

2008-06-23

Sling Blade Runner II

In an effort to understand my heuristic's performance better I've compared the chains created by icefox's (312 titles) and my program (225 titles). Interestingly enough I've found several "gaps" in my chain that really shouldn't be there. Below is a snippet of icefox's chain (the number specifies the title's position in my chain):

219 ROAD GAMES
220 GAMES PEOPLE PLAY NEW YORK
--- NEW YORK NEW YORK
221 NEW YORK COP
222 COP LAND
New York Cop has a subtree of size 7 and since New York New York's subtree is only 6 titles in size, New York Cop will always be selected by my heuristic function. But apparently there's a blind spot in my find/extend function that makes it skip New York New York even though it's not used in my chain (indicated by ---). Terribly annoying.

[Update 2008-06-25]: Now that I've "fixed" (I changed a constant from 2 to 1 ;-) the code, the longest chain found is 233 titles. A small improvement, but still. Here's the list with the titles that have been added (the number specifies the title's position in my 225-length chain):
...
036 IT HAD TO BE YOU
--- YOU CANT TAKE IT WITH YOU
037 YOU CAN COUNT ON ME
038 ME MYSELF I
039 I LOVE YOU TO DEATH
--- DEATH WISH V THE FACE OF DEATH
040 DEATH WISH
...
077 ALASKA SPIRIT OF THE WILD
--- THE WILD
078 THE WILD ONE
...
082 LIVE AND LET DIE
--- DIE MONSTER DIE
083 DIE MOMMIE DIE
...
089 DEATH WISH 3
--- 3 NINJAS
090 3 NINJAS KNUCKLE UP
...
101 EYE FOR AN EYE
--- AN EYE FOR AN EYE
102 EYE OF GOD
...
120 THE JUNGLE BOOK
--- JUNGLE BOOK
121 BOOK OF LOVE
...
220 GAMES PEOPLE PLAY NEW YORK
--- NEW YORK NEW YORK
221 NEW YORK COP
...
[Update 2008-06-26]: Modifying a few parameters creates a chain with 237 titles.

2008-06-06

Creating an .exe for a CLIPS application

Every now and then someone (on CLIPSESG) asks about how to create a standalone binary (usually a Windows .exe) for a CLIPS application they wrote. The process for doing so is described in Section 5 of the CLIPS Advanced Programming Guide. But even though that may be the correct answer to such questions it isn't always what the poster wants to hear. At least that's the impression I get.

The problem is (probably, I'm guessing here), that the description is kind of brief, contains little or no examples and is written for an experienced (or at least decent) C programmer. Which, unfortunately, kind of misses the mark since it seems that most people that ask about how to create .exe files for their applications are (relatively) new programmers and probably feel a bit lost reading that section. I know I do.

Though I can see several reasons why distributing your CLIPS application as a binary file (in any format) is a bad idea. I also know that sometimes it's the only way, so I'm not going to question your motives or try to talk you out of it. Instead. Here is a step-for-step demonstration on how to do it using PyCLIPS.

Step 0. Know the difference.
I'm not sure whether or not the steps described in CLIPS APG Section 5 results in a binary that includes the CLIPS IDE (the Dialog window and other useful things). But what I am describing here does not! So if you rely on user input at runtime you're also going to have to create a GUI for your application. I've written about that before and all of those examples are also available as PyCLIPS downloads.

Step 1: Install the tools.
The first thing you need to to do is to get all of the tools needed. You're going to have to download and install Python, PyCLIPS and py2exe. If you also need a GUI or want to try either of the wxPython CLIPS demos you should get wxPython. Make sure to get the latest version of Python (2.5.2 as I'm writing this) and releases matching it (2.5) for the other projects. Installation should be very simple since all projects provide binaries and the installers pretty much take care of themselves.

When you're done with all of the installations you can make sure that everything went alright by opening up IDLE (it should be in your start menu) and try the following:

>>> import clips
>>> import py2exe
>>> import wx
If either of those raises an exception you're going to have to re-install. Installation instructions and trouble-shooting guidance can be found on the projects' websites and in their docs.

Step 2: Wrap your application in PyCLIPS.
Once you've got all the tools in place it's time to write a wrapper for your CLIPS application. I'll use the sudoku demo (sudoku.clp and solve.clp) as base for my example application.

I'll wrap it in a Python program that reads a file as input (containing an unsolved sudoku puzzle), triggers the CLIPS program to solve the puzzle, queries CLIPS for the answer and writes the solution to stdout.

The program is in essence a copy of the on_openfile and on_solve functions from the wxPython demo so I've copy-pasted most of it. You can download a .zip with the code here.

Step 3: Write a setup.py script.
The py2exe website has tutorials and examples on how to best use it so I'll be quite brief here.

In order to compile a Python program/script into a standalone Windows executable you're going to have write a small configuration file (setup.py) which specifies the options py2exe should work with. Py2exe is an extension to distutils so you should read those docs as well.

The setup.py which I'll use looks like this:
from distutils.core import setup
import py2exe

setup(console=['sudoku.py'], data_files=[('', ['sudoku.clp', 'solve.clp'])])
there are a lot more options available but since this is more of a teaser (or a shameless plug for Python and PyCLIPS depending on how you look at it) I'll let you look into that yourself.

Step 4: Compile the application.
All we need to do to generate the .exe is to run the setup.py script in a command prompt:
C:\Program\...\py2exe-demo>setup.py py2exe
running py2exe
...
*** copy data files ***
copying C:\Python25\lib\site-packages\py2exe\run.exe -> C:\Program\...\py2exe-demo\dist\sudoku.exe

*** binary dependencies ***
Your executable(s) also depend on these dlls which are not included,
you may or may not need to distribute them.

Make sure you have the license if you distribute any of them, and
make sure you don't distribute files belonging to the operating system.

ADVAPI32.dll - C:\WINDOWS\system32\ADVAPI32.dll
USER32.dll - C:\WINDOWS\system32\USER32.dll
SHELL32.dll - C:\WINDOWS\system32\SHELL32.dll
KERNEL32.dll - C:\WINDOWS\system32\KERNEL32.dll
msvcrt.dll - C:\WINDOWS\system32\msvcrt.dll

C:\Program\...\py2exe-demo>cd dist
C:\Program\...\py2exe-demo\dist>sudoku.exe ..\Game03.txt
5 9 7 8 3 4 1 2 6
2 6 8 9 1 5 7 3 4
3 1 4 7 2 6 5 8 9
8 7 3 2 4 1 9 6 5
9 2 5 6 7 3 4 1 8
1 4 6 5 8 9 2 7 3
4 5 2 1 6 8 3 9 7
7 8 9 3 5 2 6 4 1
6 3 1 4 9 7 8 5 2
Seems to work. But listing the contents of the dist directory might make you a bit disappointed:
C:\Program\...\py2exe-demo\dist>dir
...
2006-09-19 09:52 77 824 bz2.pyd
2008-06-04 19:42 1 034 699 library.zip
2006-03-16 16:19 348 160 MSVCR71.dll
2006-09-19 09:52 2 109 440 python25.dll
2008-01-18 21:08 39 997 solve.clp
2008-01-18 21:08 5 036 sudoku.clp
2008-06-04 19:42 18 944 sudoku.exe
2006-09-19 09:52 475 136 unicodedata.pyd
2006-09-19 09:52 4 608 w9xpopen.exe
2008-03-09 04:29 1 009 152 _clips.pyd
...
Neither of the .clp files have been compiled!

If the purpose of generating the .exe is to hide implementation details, such as the CLIPS program, then you're going to have to convert the .clp files into PyCLIPS programs. It's easy enough to do, but it's boring.

You have to go through each .clp file and
1) Either remove all comments or convert them into Python comments (use # instead of ;).
2) Indent all lines in the file with 4 spaces.
3) Insert import clips as the first line.
4) Insert def load(): as the second line.
5) Wrap all definitions with a clips.Build(""" ... """).
6) Rename the file to [filename]_clp.py.

After doing that you can remove the data_files option in setup.py and modify a few lines in sudoku.py. If we change:
clips.Load("sudoku.clp")
clips.Load("solve.clp")
to
import sudoku_clp, solve_clp
sudoku_clp.load()
solve_clp.load()
and run the compilation again, the problem is "fixed". Since all of the CLIPS constructs are now embedded in Python files they will be sucked into the .exe. The "fixed" code is available here.

The steps outlined above might at least be an alternative for those of us that aren't that well-versed in C/C++. I'm sure that the same, or at least something similar, can be done with other tools and languages - but that's for someone else to write about.

The code I've used as the example ought to be pretty simple to understand, but if you have any questions or if you think I've been too brief at some point, leave a comment or send me an e-mail and we'll sort it out.

2008-05-23

Domain Specific Languages and XML

A couple of days ago Pascal Costanza wrote an interesting reply to Martin Fowler's post ParserFear. Like Pascal, I think Martin misses the point, and I agree that the problem is not that it's difficult to implement a DSL. The problem is that the bang/buck ratio favors other solutions (usually XML). But, I'd also like to point out that even when a DSL would prove useful it's still not enough to go and design your own language.

Creating a DSL from scratch is very likely to be, mostly, a duplication of effort. And it ought to be very difficult to justify the cost of that effort considering that there are so many well-designed, easily embedded, languages already. I'm thinking about languages like Python, Ruby and JavaScript to mention a few.

Don't get me wrong. I am a fan of the DSL approach (I try to stay away from XML whenever I can) but I think that it's a bit much to go to such lengths to create custom syntax instead of looking for a decent match in existing languages.

Even in situations where an XML parser is clearly sufficient, I find it better to expose a subset of an existing programming language (usually Python) to the users instead. That way I can grow the capabilities of the language easily and I can re-use all of the resources available (IDE plug-ins, tools, tutorials and other documentation) when instructing users.

The approach is similar to, or maybe exactly the same as, how Martin describes creating a DSL around a Semantic Model. The difference is that I use reflection to populate the model instead of a custom parser. I've found that this works really well with a language like Python. It's usually a lot less code than maintaining ANTLR grammar files or XML schemas/parsers and I think it usually ends up being a lot more understandable. But I guess it's really a matter of taste. You say tomato, I say tomato and all that.

2008-05-20

NotNode woes

Since the rewrite of the RuleCompiler I've been making good progress at good speed with PyRete. It might appear different to the casual observer but the reason it's taking so long to get even an alpha release out is because I've only got about an hour or two a week to spend.

Anyway, last week I spent my hours trying to add support for the NotNode. And, instead of thinking ahead, I implemented it, more or less, exactly in the same faulty manner as I did last time. Which didn't work, I might add. I knew it! But somehow, I kept hoping that changes I had made somewhere else would automagically clean up things here as well. Stupid! Stupid! Stupid!

So, now I've got a RuleCompiler that works as long as the Not-CE contains no more than one Pattern-CE. So Not(Foo(bar == var)) works fine but Not(Foo(bar == var) and Not(Bar(foo == var))) won't compile. Well. It compiles. It's just that it won't result in a correct, usable structure.

The problem is that my approach tries to tack on some state-flags to bits and pieces in the compilation process. But since the Python AST visitor uses a callback interface it's difficult (not impossible) to store state between calls. The more complex the expression being parsed the closer to insanity I get.

The easier solution, and probably the more correct one as well, is to treat whatever is within the Not-CE as a Rete Network in itself. Once compilation is done I rip out the ProductionNode from it and replace it with a NotNode (where the sub-network feeds from the right) and join it to the main network (feeding from the left).

That requires some small changes in my current implementation, but once I get it to support recursive use it's going to work with both Exists-CE and Forall-CE without me having to add an increasingly complex structure for storing state. But, then again. I knew that already! I even knew that before I started working on it. I just didn't believe it would turn out so bad this time. So, I'll spend the rest of the day kicking myself and then it's back to work. Doing it over, but right this time.

2008-05-14

Why I love Python

Two days ago I attended the first GothPy meeting here in Gothenburg. Jeppesen was kind enough to let us use one of their conference rooms for the meeting. About ten people showed up to listen to Andrew's PyCon 2007 talk about Stackless Python and to work on the BankOCR code kata.

The kata was very interesting. It was my first and I was dead certain we'd end up fighting about IDE's or whether function names should be likeThis or like_this, but everyone was very polite and helpful. Funny thing though. When we planned the meeting, Emily insisted that we should bring a computer that is not-a-Mac, preferably a Windows PC. About two minutes into the meeting there are at least six Macs on the table and I only counted to seven computers in total. But apart from the fact that no one could get along with the keyboard, the PyDev/Eclipse editor and that the unit test results where printed in red regardless of whether they failed or succeded everything went fine using a Windows-machine ;-)

We actually managed to get further than I thought we would during the kata. The last piece of code we wrote was the validation part (User story #2)

... The next step therefore is to validate that the numbers you read are in fact valid account numbers. A valid account number has a valid checksum. This can be calculated as follows:
account number:  3  4  5  8  8  2  8  6  5
position names: d9 d8 d7 d6 d5 d4 d3 d2 d1

checksum calculation:
(d1+2*d2+3*d3 +..+9*d9) mod 11 = 0
So now you should also write some code that calculates the checksum for a given number, and identifies if it is a valid account number.
We made three different versions of the is_valid method. Jacob, who had the keyboard at the time, implemented it like this:
>>> def is_valid(account_number):
... total = 0
... for i in xrange(9):
... total += (9-i)* account_number[i]
... return total % 11 == 0
and I suggested this:
>>> def is_valid(account_number):
... return sum(d* i for d, i in zip(reversed(account_number), range(1,10))) % 11 == 0
but the clear winner in our little refactoring battle was Andrew who wrote this:
>>> def is_valid(account_number):
... d9,d8,d7,d6,d5,d4,d3,d2,d1 = account_number
... return (d1+2*d2+3*d3+4*d4+5*d5+6*d6+7*d7+8*d8+9*d9) % 11 == 0
which is brilliant and reminded me of why I love Python. It looks like pseudo-code! Reading Andrew's implementation is basically like reading the specification.

NOTE! I re-wrote these functions from memory so I may have missed some little detail. My apologies if that is the case. Also, some variables have been renamed to protect the innocent.

2008-05-12

Sling Blade Runner

I found the ITA Software Hiring Puzzles almost a year ago and ever since I haven't really been able to stop thinking about one of the puzzles: Sling Blade Runner.

"How long a chain of overlapping movie titles, like Sling Blade Runner, can you find?"

Use the following listing of movie titles: MOVIES.LST. Multi-word overlaps, as in "License to Kill a Mockingbird," are allowed. The same title may not be used more than once in a solution. Heuristic solutions that may not always produce the greatest number of titles will be accepted: seek a reasonable tradeoff of efficiency and optimality.
The file MOVIES.LST contains 6561 movie titles in alphabetical order.

It *sounds* simple enough, but it's actually quite a challenging problem. In fact, if I'm not mistaken, it's one of those problems you really can't find an *optimal* solution to. Just better or worse approximations thereof.

I think I've made something like five or six, more or less serious, attempts at getting a decent solution during the year. But thinking about this problem has made me read several books, web pages and articles on various subjects like graph theory and clustering. I've written code in Lisp, CLIPS and Python to test ideas I've had. All in all; it's been a very rewarding, educational and fun year.

Last week I decided to give it another serious attempt. That resulted in (after two days and 6 hours worth of coding) a small (~200 LOC) Python program that finds a chain that is 225 titles long. I really have no idea if that is good or bad. I know that Eric Burke found a chain that is 245 titles long so I guess I could do better. But if that chain is optimal, or close to optimal, I'm quite pleased anyway.

My program uses a best-first search approach using the size of the successors tree as the heuristic. It's a rather crude approach but works surprisingly well. Since it doesn't backtrack or explore more than one path at a time it doesn't really require that much space. This also means that I can keep using my laptop while the program is running.

The program works as follows:

The first step is to build a dict containing all the successors for a title. I'm actually using the line number (from MOVIES.LST) as the identifier instead of the title itself, but this should give you an idea of what it looks like:
{ ..., 'SLING BLADE': ['BLADE II', 'BLADE RUNNER', 'BLADE TRINITY',], ... }
It takes about half a second to create the dict for this data set. It contains 6561 keys and the sum of the lengths of the values is 15157.

The second step is the search. It is done by selecting the title with the most successors (that isn't already in the list), adding it to the list and repeating until there are no more titles that can be added.

The dict is sorted by the length of the list of successors so we don't have to search using all of the 6561 titles as starting points. Since, at least in theory we'll get shorter and shorter chains the further down we go.

The gist of the search function looks like this:
>>> def best_first_search(xref, lookahead = 5, cutoff = -1):
... [...]
... for index in sorted(xref.keys(), key = lambda k: tree_size(xref, k, 0, [k,]), reverse = True):
... [...]
... while True:
... [...]
... for next in sorted(xref[curr], key = lambda k: tree_size(xref, k, lookahead, chain), reverse = True):
... if next not in chain:
... chain.append(next)
... assigned = True
... break
... if not assigned:
... if len(chain) > len(longest):
... print len(chain), "(#%s)" % (i), "%02.2fs" % (time.time()-start)
... longest = chain
... break
... [...]
... return longest
The function that calculates the size of the successors tree (see below) does so by traversing it to a certain depth. The deeper you search, the longer it takes, but there seems to be diminishing returns when using a search-depth greater than six or seven.
>>> def tree_size(xref, curr, depth, chain = None):
... if chain is None:
... chain = []
... result = 0
... if depth > 0:
... for next in xref[curr][1:]:
... if next not in chain:
... result += tree_size(xref, next, depth- 1, chain+ [next,])
... else:
... length = 0
... for title in xref[curr][1:]:
... if title not in chain:
... length += 1
... result = length
... return result
Using the above we can get a chain that is 203 titles long. It takes about two and a half minutes and is found as the 17th title searched. A chain that is 200 titles long is found when expanding the third title and takes about 20 seconds to get to. The next best chain is 202 titles long and takes about 1 minute 15 seconds to find at the ninth title expanded. The total runtime for the program using a cutoff at 20 takes about three minutes to complete. Here is the output:
161 (#1) 7.98s
177 (#2) 14.72s
200 (#3) 20.57s
202 (#9) 72.25s
203 (#17) 146.46s

1 WOMEN IN LOVE
2 LOVE WALKED IN
3 IN THIS OUR LIFE
4 LIFE OR SOMETHING LIKE IT
5 IT HAPPENED ONE NIGHT
...
199 SEX AND THE OTHER MAN
200 MAN OF THE YEAR
201 YEAR OF THE DRAGON
202 DRAGON SEED
203 SEED OF CHUCKY
203 titles in chain 182.76s
To get a longer chain we need to do some tweaking. The problem is that by selecting the title with the largest successors tree we might get long chains, but we're missing all of those little detours that make it the "longest" chain. For example, if we have a title A with two successors, B and C, where B has several successors (D, E, F, ...) and C only has B as its successor. Even though A->C->B, clearly, is the better choice my program will *always* choose the direct route A->B.

To compensate for this blind spot, every chain is expanded by trying to find longer paths between each pair of nodes in the chain (see below). Unfortunately, by adding this extra step the program requires more memory and becomes noticeably slower.
>>> def expand(xref, chain, max_depth, longest):
... [...]
... for curr, next in zip(chain[:-1], chain[1:]):
... expansions = None
... for i in range(2, max_depth+ 1):
... paths = find(xref, curr, next, i, chain)
... if paths:
... expansions = paths
... if expansions is not None:
... expansion = sorted(expansions, key = lambda k: len(k), reverse = True)[0]
... [...]
... chain[chain.index(next):chain.index(next)] = expansion
... return chain
The find function looks like this:
>>> def find(xref, curr, end, depth, chain = None):
... if chain is None:
... chain = []
... if depth > 0:
... result = []
... for next in xref[curr][1:]:
... if next not in chain:
... paths = find(xref, next, end, depth- 1, chain+ [next,])
... if paths:
... for path in paths:
... result.append([next,]+ path)
... return result
... elif end in xref[curr][1:]:
... return [[],]
... return None
I've tried various values for max_depth and 25 seems to be a good-enough value. I have found several expansions that are longer than that but they don't seem to add to the overall result anyway.

If we change the block of code that checks for and assigns the longest chain in the best_first_search function to:
...         if not assigned:
... chain = expand(xref, chain, max_depth, len(longest))
... if len(chain) > len(longest):
... print len(chain), "(#%s)" % (i), "%02.2fs" % (time.time()-start)
... longest = chain
... break
and run the program again we get the first result after about a minute and it is 205 titles long.

The longest chain is 225 titles and it is found at #134 but it takes almost 50 minutes to find. Running the whole search (500 "best" titles) takes about 4 hours. Running it with a cutoff of 150 takes about 55 minutes and produces the following output:
205 (#1) 52.72s
206 (#5) 176.38s
208 (#6) 199.54s
[10]
209 (#12) 338.63s
211 (#13) 360.17s
212 (#18) 471.40s
[20]
213 (#21) 538.34s
[30]
[40]
[50]
[60]
[70]
[80]
215 (#85) 1904.34s
[90]
[100]
[110]
218 (#116) 2592.82s
[120]
[130]
219 (#131) 2858.43s
225 (#134) 2900.19s
[140]
[150]
1 OFF THE BLACK
2 THE BLACK ANGEL
3 ANGEL EYES
4 EYES OF AN ANGEL
5 ANGEL BABY
6 BABY SECRET OF THE LOST LEGEND
7 LEGEND OF THE LOST
8 THE LOST BOYS
9 BOYS ON THE SIDE
10 SIDE OUT
11 OUT COLD
12 COLD FEVER
13 FEVER PITCH
14 PITCH BLACK
15 BLACK HAWK DOWN
16 DOWN WITH LOVE
17 LOVE WALKED IN
18 IN THIS OUR LIFE
19 LIFE OR SOMETHING LIKE IT
20 IT HAPPENED ONE NIGHT
21 ONE NIGHT STAND
22 STAND IN
23 IN THE HEAT OF THE NIGHT
24 THE NIGHT OF THE FOLLOWING DAY
25 DAY OF THE DEAD
26 THE DEAD
27 DEAD OF NIGHT
28 NIGHT OF THE LIVING DEAD
29 DEAD MAN
30 DEAD MAN WALKING
31 WALKING AND TALKING
32 TALKING ABOUT SEX
33 SEX AND THE OTHER MAN
34 MAN OF THE HOUSE
35 HOUSE OF DRACULA
36 DRACULA DEAD AND LOVING IT
37 IT HAD TO BE YOU
38 YOU CAN COUNT ON ME
39 ME MYSELF I
40 I LOVE YOU TO DEATH
41 DEATH WISH
42 WISH UPON A STAR
43 A STAR IS BORN
44 BORN AMERICAN
45 AMERICAN ME
46 ME WITHOUT YOU
47 YOU LIGHT UP MY LIFE
48 MY LIFE
49 LIFE AS A HOUSE
50 HOUSE OF FRANKENSTEIN
51 FRANKENSTEIN AND THE MONSTER FROM HELL
52 FROM HELL
53 HELL UP IN HARLEM
54 HARLEM RIVER DRIVE
55 DRIVE ME CRAZY
56 CRAZY AS HELL
57 HELL NIGHT
58 NIGHT AND THE CITY
59 THE CITY
60 CITY OF ANGELS
61 ANGELS WITH DIRTY FACES
62 FACES OF DEATH
63 DEATH SHIP
64 SHIP OF FOOLS
65 FOOLS RUSH IN
66 IN THE WINTER DARK
67 DARK STAR
68 STAR TREK IV THE VOYAGE HOME
69 HOME ALONE
70 ALONE IN THE DARK
71 THE DARK HALF
72 HALF LIGHT
73 LIGHT OF DAY
74 DAY FOR NIGHT
75 NIGHT FALLS ON MANHATTAN
76 MANHATTAN MURDER MYSTERY
77 MYSTERY ALASKA
78 ALASKA SPIRIT OF THE WILD
79 THE WILD ONE
80 ONE NIGHT WITH THE KING
81 THE KING AND I
82 I WANT TO LIVE
83 LIVE AND LET DIE
84 DIE MOMMIE DIE
85 DIE HARD
86 HARD EIGHT
87 EIGHT AND A HALF WOMEN
88 WOMEN IN LOVE
89 LOVE AND DEATH
90 DEATH WISH 3
91 3 NINJAS KNUCKLE UP
92 UP CLOSE AND PERSONAL
93 PERSONAL BEST
94 BEST OF THE BEST
95 BEST MEN
96 MEN IN BLACK
97 BLACK AND WHITE
98 WHITE HUNTER BLACK HEART
99 HEART CONDITION
100 CONDITION RED
101 RED EYE
102 EYE FOR AN EYE
103 EYE OF GOD
104 GOD TOLD ME TO
105 TO DIE FOR
106 FOR THE BOYS
107 THE BOYS
108 BOYS AND GIRLS
109 GIRLS WILL BE GIRLS
110 GIRLS GIRLS GIRLS
111 GIRLS OF SUMMER
112 SUMMER SCHOOL
113 SCHOOL OF ROCK
114 ROCK STAR
115 STAR TREK THE MOTION PICTURE
116 PICTURE BRIDE
117 BRIDE OF THE WIND
118 THE WIND AND THE LION
119 THE LION KING
120 KING OF THE JUNGLE
121 THE JUNGLE BOOK
122 BOOK OF LOVE
123 LOVE IS THE DEVIL
124 THE DEVIL RIDES OUT
125 OUT OF THE BLUE
126 BLUE CAR
127 CAR 54 WHERE ARE YOU
128 YOU ONLY LIVE ONCE
129 ONCE AROUND
130 AROUND THE BEND
131 BEND OF THE RIVER
132 THE RIVER
133 THE RIVER WILD
134 WILD THINGS
135 THINGS TO COME
136 COME AND GET IT
137 IT TAKES TWO
138 TWO FRIENDS
139 FRIENDS WITH MONEY
140 MONEY FOR NOTHING
141 NOTHING BUT TROUBLE
142 TROUBLE EVERY DAY
143 DAY OF THE WOMAN
144 THE WOMAN IN RED
145 RED RIVER
146 RIVER OF NO RETURN
147 RETURN TO HORROR HIGH
148 HIGH SPIRITS
149 SPIRITS OF THE DEAD
150 DEAD BANG
151 BANG BANG YOURE DEAD
152 DEAD MAN ON CAMPUS
153 CAMPUS MAN
154 MAN TROUBLE
155 TROUBLE IN PARADISE
156 PARADISE ROAD
157 ROAD HOUSE
158 HOUSE PARTY
159 HOUSE PARTY 3
160 3 NINJAS KICK BACK
161 BACK STAGE
162 STAGE FRIGHT
163 FRIGHT NIGHT
164 NIGHT MOTHER
165 MOTHER NIGHT
166 NIGHT ON EARTH
167 EARTH GIRLS ARE EASY
168 EASY COME EASY GO
169 GO NOW
170 NOW YOU SEE HIM NOW YOU DONT
171 DONT GO IN THE HOUSE
172 THE HOUSE OF THE DEAD
173 DEAD END
174 END OF DAYS
175 DAYS OF HEAVEN
176 HEAVEN CAN WAIT
177 WAIT UNTIL DARK
178 DARK CITY
179 CITY BY THE SEA
180 SEA OF LOVE
181 LOVE LIFE
182 LIFE IS BEAUTIFUL
183 BEAUTIFUL PEOPLE
184 PEOPLE I KNOW
185 I KNOW WHAT YOU DID LAST SUMMER
186 SUMMER CATCH
187 CATCH A FIRE
188 FIRE ON THE MOUNTAIN
189 THE MOUNTAIN MEN
190 MEN WITH GUNS
191 GUNS OF THE MAGNIFICENT SEVEN
192 THE MAGNIFICENT SEVEN
193 THE MAGNIFICENT SEVEN RIDE
194 RIDE THE HIGH COUNTRY
195 COUNTRY LIFE
196 LIFE WITH FATHER
197 FATHER OF THE BRIDE
198 BRIDE OF THE MONSTER
199 MONSTER HOUSE
200 HOUSE PARTY 2
201 2 DAYS IN THE VALLEY
202 VALLEY GIRL
203 GIRL IN THE CADILLAC
204 CADILLAC MAN
205 MAN ON FIRE
206 FIRE IN THE SKY
207 SKY HIGH
208 HIGH SCHOOL HIGH
209 HIGH CRIMES
210 CRIMES OF PASSION
211 PASSION IN THE DESERT
212 DESERT BLUE
213 BLUE STEEL
214 STEEL DAWN
215 DAWN OF THE DEAD
216 DEAD HEAT
217 HEAT AND DUST
218 DUST TO GLORY
219 GLORY ROAD
220 ROAD GAMES
221 GAMES PEOPLE PLAY NEW YORK
222 NEW YORK COP
223 COP LAND
224 LAND OF THE DEAD
225 THE DEAD POOL
225 titles in chain 3203.74s
I've got a few other ideas and strategies (beam search beam-stack search for instance) I'd like to try to see if I can get an even longer chain, but I should probably re-implement the code in Lisp to make it faster first. 50 minutes is a tad on the long side ;-)

[Update 2008-05-12] I just did some more extensive Googling and my results are not that good in comparison to the 312 titles long chain found by icefox. Back to work.

[Correction 2008-07-02]: When I say beam search I mean beam-stack search.

2008-04-28

KataMinesweeper

I've been playing around with a few of the katas at codingdojo.org these past few days. I'm new to the concept but I like the idea and since I'm going to attend a kata session led by Emily in a couple of weeks I thought I'd prepare.

Here's my best-so-far attempt at KataMinesweeper:

|def convert(rows, cols, field):
| """
| >>> print convert(1,1,['*'])
| *
| >>> print convert(2,2,['*.', '..'])
| *1
| 11
| >>> print convert(4,4,['*...', '....', '.*..', '....'])
| *100
| 2210
| 1*10
| 1110
| >>> print convert(3,5,['**...', '.....', '.*...'])
| **100
| 33200
| 1*100
| """
|
| _field = dict(zip(range(rows), [dict(zip(range(cols), f)) for f in field]))
|
| for r in range(rows):
| for c in range(cols):
| if _field[r][c] == '.':
| count = 0
| for _r,_c in [(r-1,c-1), (r-1,c), (r-1,c+1), (r,c-1), (r,c+1), (r+1,c-1), (r+1,c), (r+1,c+1)]:
| try:
| if _field[_r][_c] == '*':
| count += 1
| except:
| pass
|
| _field[r][c] = str(count)
|
| return "\n".join(["".join(f.values()) for f in _field.values()])
|
|def content(filename):
| f = open(filename)
| lines = f.readlines()
| f.close()
|
| return lines
|
|def solve(lines):
| """
| >>> solve(['4 4\\n','*...\\n','....\\n','.*..\\n','....\\n',
| ... '3 5\\n','**...\\n','.....\\n','.*...\\n',
| ... '0 0\\n'])
| Field #1:
| *100
| 2210
| 1*10
| 1110
| [BLANKLINE]
| Field #2:
| **100
| 33200
| 1*100
| [BLANKLINE]
| """
|
| l, n = 0, 1
| while True:
| rows, cols = map(int, lines[l].split())
| if rows == 0 and cols == 0:
| break
|
| print "Field #%s:" % n
| print convert(rows, cols, [line.strip() for line in lines[l+1:l+rows+1]])
| print
|
| n += 1
| l += rows+1
|
|if __name__ == '__main__':
| import doctest
| doctest.testmod()
|
| import sys
| solve(content(sys.argv[-1]))
and running it with the suggested test-case in a text-file (minesweeper.txt) gives the following output:
|C:\Playground\Katas>minesweeper.py minesweeper.txt
|Field #1:
|*100
|2210
|1*10
|1110
|
|Field #2:
|**100
|33200
|1*100
It needs more tests. I know ;-)

2008-04-16

SVN update

I haven't updated the PyRete SVN repo for some time now. Mostly because I haven't really had any (working) code I wanted to share. But no more of that. Today I managed to compile:

>>> @prodsys.rule
... def rule1(self, large = Large, medium = Medium, small = Small):
... if large(value == x, another == y) and \
... medium(value == y, another == z) and \
... small(value == z, another == x):
... pass
into a Rete Network. Seems to work as well ;-)

I'm not saying I'm done or anything. It'll probably take a while to finish all of the details and write test cases for all the relevant variants of rules that can be expressed but I'm hoping that most of the tricky parts have been sorted out. And since the public engine functions (reset, watch etc) already have been implemented once. I figure it's easy to port them to the new code. It'll probably be boring but it should be easy enough.

2008-04-11

AST to AST translation in Python

In an attempt to further simplify PyRete's rule compiler I've split it into three parts.

1) Pre-processing (splits a rule into subrules, if necessary),
2) macro-expansion (translates, for example, Exists(CE*) into Not(Not(CE*))) and
3) Rete Network construction (creates Production-, Alpha- and BetaNodes and joins them together).

The complexity of trying to handle all of that in one class just got out of hand. Also, I think this design will make PyRete a lot more flexible and easier to extend with custom constructs (given that they are not violating Python's syntax that is).

Anyway, the idea is that the Rete Network construction (step 3) should only have to deal with constructs that can be translated directly into a node in the Rete Network and that everything else should be handled in either of the two previous steps. This means, among other things, that in step 3 there'll only be Object-Pattern-CEs, Test-CEs and Not-CEs left.

I'll talk about parts of step 2 (the macro-expansion) since I think it's the most interesting part.

Unfortunately there isn't that much written about AST to AST translation in Python. Not very surprising considering that it's not generally the level of abstraction most people decide to interfer with the compilation-chain. Most attempts either cover the whole thing, from front to back, like python4ply and EasyExtend or perform low-level bytecode manipulation as bytecodehacks and psyco. Even the Python docs for the compiler module and its AST visitor class is quite sparse to say the least.

Luckily I've spent more than a year poking around on the AST level of Python code so I'm used to trial and error and having exceptions thrown in my face every now and then. The translation scheme I've implemented is not as smooth as having Common Lisp or Scheme macros but it does the job.

Here's the code for handling the Exists-CE:

>>> class ExistsCE(object):
... @classmethod
... def expand(self, node):
... nodesToVisit = [node]
... while len(nodesToVisit) > 0:
... curr = nodesToVisit.pop(0)
... for child in curr.getChildNodes():
... nodesToVisit.append(child)
... if isinstance(curr, CallFunc) and curr.node.name == "Exists":
... curr.node.name = "Not"
... curr.args = [CallFunc(Name("Not"), curr.args)]
... return node
and here's a sample usage (I've highlighted the interesting parts).
>>> ast = compiler.parse("Exists(Spam(eggs == 1, bacon = 2))")
>>> print ast
Module(None, Stmt([Discard(CallFunc(Name('Exists'), [CallFunc(Name('Spam'), [Com
pare(Name('eggs'), [('==', Const(1))]), Keyword('bacon', Const(2))], None, None)
], None, None))]))
and if we run it through the ExistsCE macro expander we get:
>>> print ExistsCE.expand(ast)
Module(None, Stmt([Discard(CallFunc(Name('Not'), [CallFunc(Name('Not'), [CallFun
c(Name('Spam'), [Compare(Name('eggs'), [('==', Const(1))]), Keyword('bacon', Con
st(2))], None, None)], None, None)], None, None))]))
which is exactly the same AST as if we had written two nested Not calls instead:
>>> print compiler.parse("Not(Not(Spam(eggs == 1, bacon = 2)))")
Module(None, Stmt([Discard(CallFunc(Name('Not'), [CallFunc(Name('Not'), [CallFun
c(Name('Spam'), [Compare(Name('eggs'), [('==', Const(1))]), Keyword('bacon', Con
st(2))], None, None)], None, None)], None, None))]))
I wonder if I can abstract away some of that boiler plate, make it more general and usable. I'll have to tinker some more but it shouldn't be impossible.

2008-03-21

1000 LOC removed

There's not many things that make me happier than being able to remove existing code. These past few days have been quite good in that respect.

Previously I compiled a Rete Network from a Python AST by passing the rule function's AST to an ASTVisitor which returned a dictionary holding information about which nodes should be created. The dictionary was then passed to a method (construct_rete) which was in charge of creating the Rete Network and figuring out in what way the nodes should be connected.

That way of doing it turned out to be an ugly beast of complexity which managed to resist all of my attempts to make it simpler. I spent more than half a year trying before I binned it completely and yesterday I updated PyRete's SVN repository with the code I've got so far.

The approach I use now is that I pass the rule function's AST to an ASTVisitor which *directly* calls the Rete implementation. This way I don't have to go through all of the trouble of maintaining a dictionary with intermediate details and the result is that I've managed to get rid of about 1000 lines of code and the compilation process is more or less understandable.

"K-I-S-S. Keep it simple, stupid. Great advice. Hurts my feelings every time." -Dwight Schrute

2008-03-14

I hate mathematics

Via Gary King, I found this little weirdness, called the 6174 problem.

  1. Pick any four distinct digits (e.g., 6, 2, 9, 0)
  2. arrange them into the largest possible integer (e.g., 9620)
  3. arrange them into the smallest (e.g., 0269)
  4. subtract the smaller from the larger (e.g., (- 9620 0269) => 9351)
  5. go back to step 2 and repeat
The cool thing is that, regardless of which digits you choose, you will end up with 6174. Here's the sequence for the digits 6, 2, 9, 0:
9620 - 0269 = 9351
9531 - 1359 = 8172
8721 - 1278 = 7443
7443 - 3447 = 3996
9963 - 3699 = 6264
6642 - 2466 = 4176
7641 - 1467 = 6174
7641 - 1467 = 6174 ...
Apparently that's the longest one, I haven't bothered to check myself but I did translate Gary's Lisp code to Python. I took the memoize implementation straight from the Python Decorator Library. The whole thing is about 100 lines (which includes doctests).

2008-03-11

Yet another post about the RuleCompiler

There's still a lot of code to be written for the RuleCompiler but I came a bit further today. I've added print statements to the parser so I can trace the creation of the Rete Network.

>>> def rule1(self, large = Large, medium = Medium, small = Small):
... salience = 10
... if large(value == x and x > 10, another == y) and \
... medium(value == y and y > x, another == z) and \
... small(value == z):
... pass

>>> parse(rule1)
# p = ProductionNode(rule = rule1)
# p.doc = None
# o1 = ObjectTypeNode(type = Small)
# o2 = ObjectTypeNode(type = Medium)
# o3 = ObjectTypeNode(type = Large)
# p.salience = 10
# x = large.value
# a1 = AlphaNode(large.value > 10)
# y = large.another
# j1 = JoinNode(large.another == medium.value)
# a2 = AlphaNode(large.another > large.value)
# z = medium.another
# j2 = JoinNode(medium.another == small.value)
I'm about 90% done. It took about 8h of programming, spread out during a week. I figure the last 10% will take at least as long. They always seem to do. But this is good. In my previous attempts it took me a lot longer to get this far.

A couple of days ago I thought I could lose the fact bindings (the method parameters) but it occured to me today that they are needed if one or more of the Facts are to be referenced in the RHS. So, the bindings stay.

And, regarding python4ply I've decided to do without it at the moment. The only thing I've found I really miss is the ability to use ? to distinguish a variable name from an attribute name. Without it there's no way a variable can have the same name as an attribute.

2008-03-09

python4ply vs a custom rule compiler

Andrew Dalke released python4ply 1.0 today. It might be useful as part of the RuleCompiler since I wouldn't have to constrain the rule syntax to Python's.

One of the main reasons for doing the rule compiler the way I am, is so I wouldn't have to re-implement a parser for Python's syntax but now that Andrew's gone and done just that... I don't know. This is from his announcement:

You might use python4ply to experiment with variations in the Python language. The PLY-based lexer and parser are much easier to change than the C implementation Python itself uses or even the ones written in Python which are part of the standard library.
It would be cool to have support for other syntaxes as well... I'll have to think about it.

2008-03-07

Progress with the RuleCompiler

Finally! It's starting to feel like I'm getting somewhere with PyRete's RuleCompiler. Earlier this week I decided to change quite a lot about the allowed syntax in a rule.

I've always kind of thought I'd have to have support for expressing rules in many different ways. For instance, I've tried to support at least three different approaches to expressing:

|CLIPS>(defrule rule
| (foo (attribute ?attr&:(> ?attr 10)))
| (bar (attribute ?attr))
| =>)
This way, which used to be the preferred way, you would write the rule as if you were writing procedural code:
>>> @prodsys.rule
... def rule(engine, foo = Foo, bar = Bar):
... if foo.attribute == bar.attribute and \
... foo.attribute > 10:
... pass
Another way, which always felt a bit weird and unnatural to me, was to use a (logical) variable:
>>> @prodsys.rule
... def rule(engine, foo = Foo, bar = Bar):
... if foo.attribute == attr and \
... bar.attribute == attr and \
... attr > 10:
... pass
The problem with it, and the reason I don't like it, is that it's not all that clear what is going to happen. There's no way of telling that attr is a logical variable and that makes it look like it's three ordinary comparisons, but it's not. In any other context the code would raise an Exception because the local variable attr is unbound and that's probably how most Python programmers will read it.

I thought that the above would cause a lot of confusion which is why I implemented another way of expressing the same thing which looks a lot more like the pattern-matching syntax of CLIPS:
>>> @prodsys.rule
... def rule(engine, foo = Foo, bar = Bar):
... if foo(attribute == attr and attr > 10) and \
... bar(attribute == attr):
... pass
This is, of course, also slightly problematic but at least there's no interpretation of the code that completely shadows how it's being used in PyRete.

It wasn't until I decided, quite recently, that I'll only support the last type of syntax (pattern matching-style) that I started to get somewhere with the RuleCompiler. It also meant that I could get rid of the object bindings in the parameter list.

The above rule should be written as:
>>> @prodsys.rule
... def rule(engine):
... if Foo(attribute == attr and attr > 10) and \
... Bar(attribute == attr):
... pass
It's only too bad that it takes me so long to figure these things out.