May 02 2020

Declarative Programming Tastes Better

There was a TED Talk interview back in 2016 with Linus Torvalds. He mentions he likes to work with developers who have “good taste”. In the code example denoting “good taste”, he eliminates branching in the function.

For such a small change, the concept was quite mind blowing. The code became much simpler and less error-prone. On a similar note, I felt that Name that Algorithm: #1 fit into this family. Although instead of removing complexity, we were simply hiding it in another function.

After watching this video I wanted to try my hand at removing branches. The following example converts an if/elif chain into a more declarative approach. Declarative programming tends to prevent unnecessary branching due to its lazy loading nature.

Lets start with the bad tasting code.

def get_driver(driver_type, opts={}):
  driver = None

  if driver_type == 'LOCAL':
    driver = webdriver.Firefox()
  elif driver_type == 'REMOTE':
    url = opts.get('DRIVER_URL')
    driver = webdriver.Remote(
        command_executor=url,
        desired_capabilities=DesiredCapabilities.FIREFOX)
    else:
        raise AttributeError('driver type not specified')

   driver.delete_all_cookies()
   return driver

On the surface it doesn’t look too bad. It has a clear path on constructing either a Firefox driver or a Remote driver. This is similar to how a factory pattern might resolve a constructor. However I knew this could become more intuitive with a declarative paradigm. Let’s take a look on how I refactored this.

def get_driver(driver_type, opts={}):
  DRIVER_TYPES = { 'LOCAL': webdriver.Firefox, 'REMOTE': webdriver.Remote }
  DRIVER_OPTS = {
      'LOCAL': {},
      'REMOTE': {
          'command_executor': opts.get('command_executor'),
          'desired_capabilities': opts.get('desired_capabilities'),
      }
  }

  driver_creator = DRIVER_TYPES[driver_type]
  driver = driver_creator(**DRIVER_OPTS[driver_type])
  driver.delete_all_cookies()
  return driver

I moved each strategy to a DRIVER_TYPES dictionary. I also created a DRIVER_OPTS dictionary. This way each driver type can opt-in to whatever opts are needed. These opts ultimately come from the environment variables. This ensures that whatever the user passes as a driver configuration will end up in the driver. DesiredCapabilities is passed from above now too.

Ideally the DRIVER_TYPES would be moved out of the get_driver function. Since the refactored method is only 13 lines, I will keep it as is. In the case that this method grows down the road, I will likely break this up into smaller functions.

Finally, I changed the AttributeError to a KeyError. This is implicitly thrown when driver_creator = DRIVER_TYPES[driver_type] fails.

My diff tool says that I removed 11 lines and added 11 lines. This leaves the code in a fairly similar position that it was in before. However this time declarative programming weeds out any explicit branching.

Another big win is that it is obvious which strategies there are to choose from. It is also obvious which options LOCAL needs and which options REMOTE needs. The best part is that these are the first two lines in the method. Separating these two lines from the instantiation makes readability much better. Hopefully this falls into the category of good taste, and that I can keep a little bit of it as I move forward on the developer journey.