Wishful Coding in Python: A Problem Solving Philosophy
What is Wishful Code?
Wishful code is code which calls functions that don't yet exist.
Wishful coding is a strategy/philosophy for recursively solving big problems. It goes like this:
- Define the top-level function which, when called, will solve your problem. Don't write the body of the function yet! Just the name and the input parameters.
- Write tests which demonstrate the responsibilities of this function.
- Write "wishful" code to fill out the body of this function.
- Now you should have several functions which don't yet exist. Each of these is a new problem which can be solved wishfully.
Solving a Problem Wishfully
The other day I wanted to write a program to check to see if a Sudoku is valid. These are the first 2 lines I wrote:
def check_sudoku(grid):
pass
And this is where the "philosophy" comes in. At this point, I'm now thinking of my problem in a hyper-focused way. The only thing I have to do is write this function correctly.
But what does "correctly" mean? This is where tests come in.
The next code I write is this:
def test():
# rows add to 9, columns add to 9, and the
# nine 3 x 3 squares all contain digits 1 - 9
valid_grid = [[4,3,5,2,6,9,7,8,1],
[6,8,2,5,7,1,4,9,3],
[1,9,7,8,3,4,5,6,2],
[8,2,6,1,9,5,3,4,7],
[3,7,4,6,8,2,9,1,5],
[9,5,1,7,4,3,6,2,8],
[5,1,9,3,2,6,8,7,4],
[2,4,8,9,5,7,1,3,6],
[7,6,3,4,1,8,2,5,9]]
invalid_grid = [[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8,9]]
assert(check_sudoku(valid_grid))
assert(not check_sudoku(invalid_grid))
print "Tests pass."
Next I press return
about 20 times and write my favorite line of code way down at the bottom...
test()
A Singular Focus
At this point I run the code. I see the enemy and its name is AssertionError. I now have a singular purpose in life. I must eliminate that error. These tests shall pass.
This psychological hack is really helpful for me. I sat down to write a Sudoku checker. An earlier version of me would have allowed this project to balloon. What started as a checker may become a game. And what kind of programmer would write a game like Sudoku without also writing an AI to play it? And why stop at 3 x 3 grids? Why not N x N? What about N x M? What would that even mean?!
But not anymore. I've got tests to pass.
Before you continue, brush up on the rules of Sudoku if you need to.
check_sudoku
Wishfully
Writing Let's solve the whole problem at once. This is what my solution looks like.
def check_sudoku(grid):
return (check_rows(grid) and
check_columns(grid) and
check_squares(grid))
def check_rows(grid):
pass
def check_columns(grid):
pass
def check_squares(grid):
pass
And this is "wishful coding". Those functions that I'm relying on don't exist yet. But in essence the problem is solved. Now I just have to solve three smaller problems. And how do you solve problems? Wishfully and recursively of course!
Wishful all the Way Down
My next step was to check the rows. Better add a test first.
# this code goes into test()
passes_check_rows = [range(1,10) for _ in range(9)]
assert(check_rows(passes_check_rows))
assert(not check_sudoku(passes_check_rows))
And now the code...
def check_rows(grid):
for row in grid:
if not check_row(row):
return False
return True
def check_row(grid):
pass
I don't know how I'm going to write the check_row
function, but I don't really care right now. On to check_columns
.
def check_columns(grid):
return check_rows(transpose(grid))
This code is similar enough to the check_rows
function (which I've already written a test for) that I'm not going to test check_columns
directly. But I will test transpose
.
g1_before = [[1,2],
[3,4],
[5,6]]
g1_after = [[1,3,5],
[2,4,6]]
assert(transpose(g1_before) == g1_after)
And now I just have to pass this test...
def transpose(grid):
num_rows = len(grid)
num_cols = len(grid[0])
new_grid = [[0 for i in range(num_rows)] for j in range(num_cols)]
for i in range(len(grid)):
for j in range(len(grid[0])):
new_grid[j][i] = grid[i][j]
return new_grid
Now to check the squares. To create a test case that would fail I just modified one of the entries in the passing test case.
squares_fail = [[4,3,5,2,6,9,7,8,1],
[6,8,2,5,7,1,4,9,3],
[1,9,7,8,3,4,5,6,2],
[8,2,6,1,9,5,3,4,7],
[3,7,4,6,8,2,9,1,5],
[9,5,1,7,4,3,6,2,8],
[5,1,9,3,2,3,8,7,4],
[2,4,8,9,5,7,1,3,6],
[7,6,3,4,1,8,2,5,9]]
assert(not check_squares(squares_fail))
And then the code.
def check_squares(grid):
squares = make_squares(grid)
for square in squares:
if not check_square(square):
return False
return True
def make_squares(grid):
pass
def check_square(square):
pass
This was exceptionally wishful. I haven't even figured out how I want to represent a "square" yet, but I like thinking about it this way and if it turns out not to work, changing will be simple. Now let's figure out how to represent a square and capture that in a test.
grid_of_zeros = [[0 for i in range(9)] for j in range(9)]
square_of_zeros = [[0 for i in range(3)] for j in range(3)]
squares = [square_of_zeros for _ in range(9)]
assert(make_squares(grid_of_zeros) == squares)
good_square = [[1,3,5],
[2,4,6],
[7,8,9]]
bad_square = [[1,1,5],
[2,4,6],
[7,8,9]]
assert(check_square(good_square))
assert(not check_square(bad_square))
You know the drill...
def make_squares(grid):
squares = []
for n in range(9):
i = (n / 3) * 3 # 0,0,0,3,3,3,6,6,6
j = (n % 3) * 3 # 0,3,6,0,3,6,0,3,6
square = [row[i : i+3] for row in grid[j : j+3]]
squares.append(square)
return squares
def check_square(square):
nums = [num for row in square for num in row]
return check_row(nums)
I represent a square as a 3x3 grid (list of lists) but then flatten this into a single row in check_square
, which lets me use my check_row function.
Almost there. On to check_row
.
def check_row(row):
return (sum(row) == sum(range(10)) and len(set(row)) == 9)
Why I Love Wishful Coding
Wishful coding has a few benefits that I really love
1. Cognitive Cleanliness
I transformed one medium-sized problem into three small ones the instant I wrote
def check_sudoku(grid):
return (check_rows(grid) and
check_columns(grid) and
check_squares(grid))
This gave me the freedom to think of each of these smaller problems in isolation. Since I've already specified how each of these functions interact with the larger program, I can safely put that out of my mind as I devote all of my attention to writing check_rows
.
2. Readability
The most important code is the highest level code. In this case, it's the check_sudoku
function. By writing that code first I didn't have to constrain myself to the idiosyncrasies of the code I'd already written. Now my top-level function is clean, clear, and entirely self-documenting.
3. Forced Focus
When you start with the highest-level code, you immediately impose some discipline on yourself. The goal is to pass the test. That's it.
This is actually an old, effective technique that I first practiced as a mainframe COBOL programmer a few decades ago. It’s nice seeing it rediscovered today. Thanks for the article.
To be fair, your transpose implementation can be replaced with
transpose = lambda rows: list(map(list, zip(*rows)))
The automated testing bit here is a necessity, otherwise you risk getting confused about where you wanted to go with your project.