An exchange with Bill triggers some ideas for the flood. It’s 0330 hours. Let’s see what we can do.

I woke up and can’t get back to sleep. Since I have a couple of ideas for the flood, I decided to get up and work on them. I have a few things in mind, one of them slightly larger than the others.

  • Rename the frontier and examined variables for clarity, and any others that seem to follow;
  • Break out chunks of the code into separate methods, again for naming clarity of those chunks;
  • Create a “Method Object” to do the actual work. Give it a fluent interface.

Obviously that last one is the larger. I envision a Flood object, into which we’ll move all the code. I’m not sure whether we’ll keep a flood method on cell or not, but I’m leaning toward replacing it with the Flood facility.

Despite it being the hardest bit and despite the obscene hour, I think we’ll start with the Flood object. Rather than write new tests for it, we’ll try to refactor the existing flood to create and use the new class.

To that end, here’s Cell.flood:

class Cell:
    def flood(self, *,
              select=lambda cell: True,
              next_value=lambda cell, value: 0,
              initial_value=0,
              randomness=0.0):
        frontier = {self: initial_value}
        examined = {self}
        while frontier:
            current_cell = next(iter(frontier))
            current_value = frontier.pop(current_cell)
            yield current_cell, current_value
            examined.add(current_cell)
            neighbors = self._get_neighbors(current_cell, randomness)
            for neighbor in neighbors:
                if (neighbor not in examined and
                    select(neighbor) and
                    neighbor not in frontier):
                        frontier[neighbor] = next_value(current_cell, current_value)

    @staticmethod
    def _get_neighbors(next_cell, randomness):
        if random.random() < randomness:
            return next_cell.random_neighbors
        return next_cell.neighbors

I’ll demand the object into being by just creating it at the top of flood:

class Cell:
    def flood(self, *,
              select=lambda cell: True,
              next_value=lambda cell, value: 0,
              initial_value=0,
              randomness=0.0):
        flooder = Flood(self)
        ...

class Flood:
    def __init__(self, origin):
        self.origin = origin

That passes. Commit: working toward Flood object.

Now what? I think we’ll call our flooder, passing the four parameters from flood in via methods:

    def flood(self, *,
              select=lambda cell: True,
              next_value=lambda cell, value: 0,
              initial_value=0,
              randomness=0.0):
        flooder = Flood(self)
        flooder.select(select)
        flooder.next_value(next_value)
        flooder.initial_value(initial_value)
        flooder.randomness(randomness)

class Flood:
    def __init__(self, origin):
        self._origin = origin
        self._select = lambda cell: True,
        self._next_value = lambda cell, value: 0
        self._initial_value = 0
        self._randomness = 0.0

    def select(self, function):
        self._select = function

    def next_value(self, function):
        self._next_value = function

    def initial_value(self, result):
        self._initial_value = result

    def randomness(self, fraction):
        self._randomness = fraction

Now Flood knows everything that Cell.flood knows. Commit a save point.

Let’s posit a method flood and move our operational code over. It’ll need a bit of tweaking but with luck not much.

After I got the first bit right, i.e. as you see it above, the rest was pretty straightforward. Here, with renaming the class to Flooder, because it seemed better:

class Flooder:
    def __init__(self, origin):
        self._origin = origin
        self._select = lambda cell: True
        self._next_value = lambda cell, value: 0
        self._initial_value = 0
        self._randomness = 0.0

    @staticmethod
    def default_select(cell):
        return True

    def select(self, function):
        self._select = function
        return self

    def next_value(self, function):
        self._next_value = function
        return self

    def initial_value(self, result):
        self._initial_value = result
        return self

    def randomness(self, fraction):
        self._randomness = fraction
        return self

    def flood(self):
        frontier = {self._origin: self._initial_value}
        examined = {self._origin}
        while frontier:
            current_cell = next(iter(frontier))
            current_value = frontier.pop(current_cell)
            yield current_cell, current_value
            examined.add(current_cell)
            neighbors = self._get_neighbors(current_cell, self._randomness)
            for neighbor in neighbors:
                if (neighbor not in examined and
                    self._select(neighbor) and
                    neighbor not in frontier):
                        frontier[neighbor] = self._next_value(current_cell, current_value)

    @staticmethod
    def _get_neighbors(next_cell, randomness):
        if random.random() < randomness:
            return next_cell.random_neighbors
        return next_cell.neighbors

And in Cell:

class Cell:
    def flood(self, *,
              select=lambda cell: True,
              next_value=lambda cell, value: 0,
              initial_value=0,
              randomness=0.0):
        for c, v in Flooder(self)\
            .select(select)\
            .next_value(next_value)\
            .initial_value(initial_value)\
            .randomness(randomness)\
            .flood():
            yield c, v

This is all green, all 50-odd tests. Commit: Cell.flood uses Flooder object.

Now I did have some improvements to the flood itself in mind as well:

    def flood(self):
        to_be_delivered = {self._origin: self._initial_value}
        delivered = set()
        while to_be_delivered:
            current_cell = next(iter(to_be_delivered))
            current_value = to_be_delivered.pop(current_cell)
            yield current_cell, current_value
            delivered.add(current_cell)
            neighbors = self._get_neighbors(current_cell, self._randomness)
            for neighbor in neighbors:
                if (neighbor not in delivered and
                    self._select(neighbor) and
                    neighbor not in to_be_delivered):
                        to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

Renamed frontier to to_be_delivered and examined to delivered. Noticed that delivered could be initialized to empty. Green commit: refactoring

Move the delivered.add line up, extract method:

    def flood(self):
        to_be_delivered = {self._origin: self._initial_value}
        delivered = set()
        while to_be_delivered:
            current_cell, current_value = self.next_to_deliver(to_be_delivered, delivered)
            yield current_cell, current_value
            neighbors = self._get_neighbors(current_cell, self._randomness)
            for neighbor in neighbors:
                if (neighbor not in delivered and
                    self._select(neighbor) and
                    neighbor not in to_be_delivered):
                        to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def next_to_deliver(self, to_be_delivered, delivered):
        current_cell = next(iter(to_be_delivered))
        current_value = to_be_delivered.pop(current_cell)
        delivered.add(current_cell)
        return current_cell, current_value

Make to_be_delivered and delivered instance variables:

    def flood(self):
        self.to_be_delivered = {self._origin: self._initial_value}
        self.delivered = set()
        while self.to_be_delivered:
            current_cell, current_value = self.next_to_deliver(self.to_be_delivered, self.delivered)
            yield current_cell, current_value
            neighbors = self._get_neighbors(current_cell, self._randomness)
            for neighbor in neighbors:
                if (neighbor not in self.delivered and
                    self._select(neighbor) and
                    neighbor not in self.to_be_delivered):
                        self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

Remove the parameters from next_to_deliver, reference the members.

    def flood(self):
        self.to_be_delivered = {self._origin: self._initial_value}
        self.delivered = set()
        while self.to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            neighbors = self._get_neighbors(current_cell, self._randomness)
            for neighbor in neighbors:
                if (neighbor not in self.delivered and
                    self._select(neighbor) and
                    neighbor not in self.to_be_delivered):
                        self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def next_to_deliver(self):
        current_cell = next(iter(self.to_be_delivered))
        current_value = self.to_be_delivered.pop(current_cell)
        self.delivered.add(current_cell)
        return current_cell, current_value

Change _get_neighbors to use instance variable:

    def flood(self):
        self.to_be_delivered = {self._origin: self._initial_value}
        self.delivered = set()
        while self.to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            neighbors = self._get_neighbors(current_cell)
            for neighbor in neighbors:
                if (neighbor not in self.delivered and
                    self._select(neighbor) and
                    neighbor not in self.to_be_delivered):
                        self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def _get_neighbors(self, next_cell):
        if random.random() < self._randomness:
            return next_cell.random_neighbors
        return next_cell.neighbors

Green, commit. Forgot to commit the _get_neighbors. Slow down! (I get on a roll, seeing next moves and making them. Better to commit each one.)

Inline the get neighbors … I think that’ll be good …

    def flood(self):
        self.to_be_delivered = {self._origin: self._initial_value}
        self.delivered = set()
        while self.to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            for neighbor in self._get_neighbors(current_cell):
                if (neighbor not in self.delivered and
                        self._select(neighbor) and
                        neighbor not in self.to_be_delivered):
                    self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

Commit. Extract that whole for loop thing to new method enqueue_relevant_neighbors:

    def flood(self):
        self.to_be_delivered = {self._origin: self._initial_value}
        self.delivered = set()
        while self.to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            self.enqueue_relevant_neighbors(current_cell, current_value)

    def enqueue_relevant_neighbors(self, current_cell, current_value):
        for neighbor in self._get_neighbors(current_cell):
            if (neighbor not in self.delivered and
                    self._select(neighbor) and
                    neighbor not in self.to_be_delivered):
                self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

Green, commit. The flood method looks rather nice now. Let’s see about fixing up the enqueue one.

    def enqueue_relevant_neighbors(self, current_cell, current_value):
        for neighbor in self._get_neighbors(current_cell):
            if (self.is_relevant(neighbor)):
                self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def is_relevant(self, neighbor):
        return (neighbor not in self.delivered and
                self._select(neighbor) and
                neighbor not in self.to_be_delivered)

Better but what I really want is this, which I’ll just type in as Wishful Thinking:

    def enqueue_relevant_neighbors(self, current_cell, current_value):
        for neighbor in self._relevant_neighbors(current_cell):
            self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def _relevant_neighbors(self, cell):
        return (neighbor for neighbor in self._get_neighbors(cell) if self.is_relevant(neighbor))

Green. Commit then look at the whole thing and assess.

class Flooder:
    def __init__(self, origin):
        self._delivered = None
        self._to_be_delivered = None
        self._origin = origin
        self._select = lambda cell: True
        self._next_value = lambda cell, value: 0
        self._initial_value = 0
        self._randomness = 0.0

    def select(self, function):
        self._select = function
        return self

    def next_value(self, function):
        self._next_value = function
        return self

    def initial_value(self, result):
        self._initial_value = result
        return self

    def randomness(self, fraction):
        self._randomness = fraction
        return self

    def flood(self):
        self._to_be_delivered = {self._origin: self._initial_value}
        self._delivered = set()
        while self._to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            self.enqueue_relevant_neighbors(current_cell, current_value)

    def next_to_deliver(self):
        current_cell = next(iter(self._to_be_delivered))
        current_value = self._to_be_delivered.pop(current_cell)
        self._delivered.add(current_cell)
        return current_cell, current_value

    def enqueue_relevant_neighbors(self, current_cell, current_value):
        for neighbor in self._relevant_neighbors(current_cell):
            self._to_be_delivered[neighbor] = self._next_value(current_cell, current_value)

    def _relevant_neighbors(self, cell):
        return (neighbor
                for neighbor in self._get_neighbors(cell)
                if self.is_relevant(neighbor))

    def is_relevant(self, neighbor):
        return (neighbor not in self._delivered and
                neighbor not in self._to_be_delivered and
                self._select(neighbor) )

    def _get_neighbors(self, next_cell):
        if random.random() < self._randomness:
            return next_cell.random_neighbors
        return next_cell.neighbors

Summary

Just shy of 60 lines with all those broken out methods. It was 25 in Cell class, and we have added a few blank lines and 25 lines just in the fluent interface. We’re going to be glad we have that if and when we remove the convenience method from Cell and ask users to use Flooder to get their fills done.

What I like most about this is the code for flood itself:

    def flood(self):
        self._to_be_delivered = {self._origin: self._initial_value}
        self._delivered = set()
        while self._to_be_delivered:
            current_cell, current_value = self.next_to_deliver()
            yield current_cell, current_value
            self.enqueue_relevant_neighbors(current_cell, current_value)

I do think that is rather nice. We’ll see what we think in a few days. But here at 0530, I like it. Now I’m gonna try to get back to sleep.

See you next time!