Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Codementor: Wishful Coding in Python: A Problem Solving Philosophy

$
0
0

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:

  1. 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.
  2. Write tests which demonstrate the responsibilities of this function.
  3. Write “wishful” code to fill out the body of this function.
  4. Now you should have several functions which don’t yet exist. Each of theses 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.

Writing check_sudoku Wishfully

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.


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>