My eldest son is a first-year computer science student at a reputable local tertiary education institute. A few days ago, we pair-programmed on one of his assignments. A pleasing upshot of our collaboration was his insight into how much less confusing code can be to work with when it reveals intent.
In my opinion, it was a crying shame that the assignment was marked solely on the correctness of the programming. It would have been refreshing to see a proportion of the marks allocated towards whether programmers could readily understand what was going on. The challenge with a vast number of existing programs is not their correctness at a certain point in time but the ability for software developers to understand their behaviour and make appropriate changes. That is the real problem facing our industry.
I suppose the faculty didn’t do it that way as a human would have had to check the answers. As it stood, a computer could do the job.
Computers are great at checking whether a program works as expected but terrible at verifying ease of legibility for humans.
My son and I had to write a
put() method for a hashtable class.
put() would insert a key into a hashtable and store it in a slot identified by a computed hash index. A collision occurs when a slot is already filled—it already has a key in it. In that case, we were to recalculate the hash index until we found an empty slot for the key. Then insert the key in the empty slot. Job complete.
Below is our original Python listing for
def put(self, key): if self.__len__() == self.__size: raise IndexError("ERROR: The hash table is full.") index = self.get_hash_code(key) if self.__slots[index] == None: self.__slots[index] = key else: index = self.get_new_hash_code_quadratic_probing(index, 1) self.__slots[index] = key
What is going on here? Yes, it’s not obvious.
We then reworked
put() to better reveal intent:
def put(self, key): if self.is_full(): raise IndexError("ERROR: The hash table is full.") index = self.calc_index(key) if self.is_empty_slot(index): self.fill_slot(index, key) else: index = self.recalc_index(index) self.fill_slot(index, key) def is_full(self): return self.__len__() == self.__size def calc_index(self, key): return self.get_hash_code(key) def is_empty_slot(self, index): return self.__slots[index] == None def fill_slot(self, index, key): self.__slots[index] = key def recalc_index(self, index): return self.get_new_hash_code_quadratic_probing(index, 1)
put() nearly reads like English:
- If the hashtable is full, then
- Raise an IndexError exception reporting that the hashtable is full.
- Use the key to calculate the hash index.
- If the slot at the index is empty, then
- Fill the slot at the index with the key,
- Recalculate a new hash index and
- Fill the slot at the new index with the key.
Isn’t that easier to understand? The intention-revealing
put() method is about high-level policy. For example, it should not concern itself with recalculating the index during a collision (i.e. full slot). We hide this detail inside the
recalc_index() function. Closer inspection will reveal it to use a quadratic probing function, whatever that is. It’s a detail we can hide from view. If we wanted to, we could change the collision resolution to another algorithm, and only
recalc_index() would change;
put() would not need to be modified.
Hide distracting mechanisms to reveal critical policy and intent.
Reveal intent in your programming.