It's not Naming That's Hard—It's Types

March 15, 2016 📬 Get My Weekly Newsletter

Katrina Owen wrote an interesting piece on SitePoint Ruby called “What’s in a Name? Anti-Patterns to a Hard Problem”. She identifies a lot of pitfalls around naming and method design, but the solutions to the problems she’s identified aren’t as much about naming as they are about using data types effectively.

I’m having a hard time with this statement from her post:

Type information is just not that compelling.

Type information is everything. Every line of code is filled with types, and the correctness of code, as well as our ability to use it properly, relies on knowing type information. Just because Ruby has “duck typing” doesn’t mean it has no types.

And, types are a better way to solve the problems Katrina identifies in her post.

She starts with this routine, arguing that the names are bad since they encode type information:

def anagrams(string, string_array)
  string_array.each do |str|
    str != string && same_alphagram?(string, str)
  end
end

For me, it’s plainly obvious how to properly use this method: I pass in a string and an array of strings. Katrina’s improved version makes it harder to know what I’m expected to pass in:

def anagrams(subject, candidates)
  candidates.each do |candidate|
    subject != candidate && same_alphagram?(subject, candidate)
  end
end

What is a “subject”? What is a “candidate”? My first guess would be some sort of Subject or Candidate class. The author of this code had something in mind that I should be passing in, but it’s no longer clear. With no guidance I would have to read the source to same_alphagram? (and whatever its dependencies are).

The real problem is that a string is not a word and an array of strings is not a set of words. We shouldn’t be using strings to solve this problem, and no efforts of naming will change that. What the anagrams method is trying to do is tell us if one word has anagrams in a set of other words. That says to me we might need a word class.

class Word
  def initialize(string)
    @string = word
  end

  def to_s
    @string
  end
end

This may seem superfluous, but we have now named the thing we are operating on. We’ve also created a place for ourselves to put code about this type of data. For example, a word should only contain alphabetics and spaces.

class Word
  def initialize(string)
    unless string =~ /^[\a\s]+$/
      raise ArgumentError, "#{string} is not a word" 
    end
    @string = word
  end
end

We’ve now described—in code—what a word is. We’ve written code that explains what the parameters to anagrams actually are supposed to be. And we communicate that by naming the parameters after the data type:

def anagrams(word, candidate_words)
  candidate_words.each do |candidate_word|
    word != candidate_word && same_alphagram?(word, candidate_word)
  end
end

With an actual data type available to us—instead of a string—we can also get rid of that pesky same_alphagram? free-floating method.

class Word

  def letters
    @string.chars
  end

  def same_alphagram?(other_word)
    self.letters.sort == other_word.letters.sort
  end
end

Now, our anagrams routine is a bit better:

def anagrams(word, candidate_words)
  candidate_words.each do |candidate_word|
    word != candidate_word && word.same_alphagram?(candidate_word)
  end
end

While the names in the original routine weren’t great, the solution wasn’t to mask the method’s intent and proper use by using different names. The solution is use code to describe the parameters.

But wait. letters is returning an array of strings. Shouldn’t it return a Letter?

Yup. Let’s do that.

class Letter
  def initialize(char)
    unless char =~ /^[\a\s]$/
      raise ArgumentError,"'#{char}' is not a letter" 
    end
    @char = char
  end

  def to_s
    @char
  end

  def ==(other_letter)
    self.to_s == other_letter.to_s
  end

  def <=>(other_letter)
    self.to_s <=> other_letter.to_s
  end
end

Before we re-implement letters, let’s take a moment. We have duplicated code now in the initializers of these two classes. Because we now have a Letter class that describes a letter, and we know that a Word is an ordered list of Letters, we can change our implementation of Word to make that abundandtly clear.

class Word

  attr_reader :letters

  def initialize(string)
    @letters = string.chars.map { |char|
      Letter.new(char)
    }
  rescue ex => ArgumentError
    raise ArgumentError, "'#{string}' is not a word: #{ex.message}"
  end

  def to_s
    @letters.map(&:to_s).join("")
  end

  def same_alphagram?(other_word)
    self.letters.sort == other_word.letters.sort
  end
end

This may seem like a lot of code, and possibly even ridiculous, but if we really are writing code that does anagrams, doesn’t it make sense to have classes to describe the building blocks of our domain?

Strings (and Hashes) are great for exploring your domain, but once you understand your domain, data types will make your code easier to understand and easier to change. It also alleviates you from stressing about parameter names. Thinking in Types will make your code better and make you a better programmer. They also help greatly in naming things.