In celebration of yield from

14 Oct 2014

A conversation with current Hacker Schooler Cerek about writing a boggle solver in Haskell motivated me to squeeze in writing up a solution in Python yesterday morning between appointments with Hacker Schoolers.

The strategy I followed was to generate all candidate words and check them for validity with a dictionary, with the optimization of not actually traversing the entire tree of possible words. I wanted to separate building the candidate words and checking them for validity, but allow the two processes to communicate to eliminate branches that weren’t promising: if no English words begin with abcd don’t bother investigating the branch of the tree of candidate words that begin with these letters, like abcdh. Coroutines seemed like a good fit.

I started coding in Python 2 because that’s still my default for throw-away scripts, but ended up with code like this in a few places:

def candidates(board, row, col, visited, candidate=''):
    if (yield candidate): # whether this branch is a dead end
        return
    for c, (y, x) in unvisited_neighbors(board, visited, row, col):
        visited.add((y, x))
        cands = candidates(board, y, x, visited, candidate+c)
        for cand in cands: break
        while True:
            try:
                cand = cands.send((yield cand))
            except StopIteration:
                break
        visited.remove((y, x))

Then remembered - of course, yield from! I should be using Python 3 for this:

def candidates(board, row, col, visited, candidate=''):
    if (yield candidate): # whether this branch is a dead end
        return
    for c, (y, x) in unvisited_neighbors(board, visited, row, col):
        visited.add((y, x))
        yield from candidates(board, y, x, visited, candidate+c)
        visited.remove((y, x))

Having independently come up with something pretty close to the functionality of yield from solidifies how that construct works for me. See the vim :TOhtml-generated diff for the full code.1 One more reason to be using Python 3 by default! :)

  1. The ugly timing code was for figuring out that the Python 3 code was 40% faster. It’d be fun to investigate where this came from. Yes, I had to double check the meaning of “40% faster” before using it here, and don’t think it’s a very clear thing to say.