Your Program is a Special and Unique Snowflake

Joe Ferris

Everyone’s special…which is another way of saying no one is.

— The Incredibles

Developers live in a world of abstraction. Rails abstracts away the details of web requests, providing you with a world of routes and controllers in place of requests and responses. Ruby hides the fact that you’re constantly allocating and deallocating memory. Ruby doesn’t have to tell the computer how to allocate memory, though, because it’s written in C.

We can’t live without abstractions, because there’s too much detail in our world to hold in all at once. We have to build bigger concepts out of smaller concepts until we can fit them all in our human brains.

One of the great challenges of writing usable code is deciding what and when to abstract. We want just enough abstraction that we can hold the problem in our heads, but not so much that we can’t tell what’s actually going on.

One technique for keeping that balance is to use fewer abstractions by avoiding special cases. Specializations can be easy to learn because they’re more concrete, but we need more special cases to solve most problems because each special case only applies to a specific situation.

Custom Syntax

Let’s look at a special case in Ruby:

for user in users
  puts user.email
end

The for loop is rarely used in Ruby. Most authors prefer to write:

users.each do |user|
  puts user.email
end

Why? One is a special case, and one is an abstraction.

The for loop introduces new syntax and keywords for a single concept. The contents will be performed for each item in a list. Once you’ve learned how for loops work, you know how to do one thing. You’ve gained one ability for one keyword. That’s not a great return on your investment. If you’re going to fit a lot of programming concepts in your head, you’ll need to do better!

The each method builds on widely-used concepts from Ruby: methods and blocks. To truly understand each, you need a solid understanding of some more abstract concepts. This takes longer, but you’ll be well-rewarded for your efforts: almost every problem in Ruby is phrased in terms of objects, methods, and blocks.

Another nice aspect of the each abstraction is that we can build it ourselves in Ruby:

class Array
  def each(&block)
    unless empty?
      first, *rest = self
      block.call(first)
      rest.each(&block)
    end
  end
end

Our implementation introduces some new concepts we have to understand: conditionals, the unless keyword, the empty? and call methods, array globbing, recursion, and block arguments. Veteran Rubyists may not realize how many concepts are involved in what we consider a basic building block of application logic, but you had to learn every one of these concepts at some point to use them effectively.

On the other hand, we can’t implement a for loop ourselves. We can’t break it down any further, because it’s a special syntax and doesn’t build on other concepts from Ruby.

If you can implement a new idea in terms of other ideas that are already defined, you don’t need as many abstractions in total. How many ideas can you eliminate from your program by building and reusing abstractions?

Empty Cases

Many abstractions are just a more specific version of another existing abstraction. Let’s look at a popular example: nil.

We use nil to represent something that could be there, but isn’t. Some users may have emails, while others don’t. Those users will return nil when asked for their email.

User.new(email: "user@example.com").email
=> "user@example.com"

User.new(email: nil).email
=> nil

This seems simple and we need to represent the case of a missing email address. However, this greatly complicates our code. We can’t just ask for an email and use it; we have to verify that it exists:

users.each do |user|
  unless user.email.nil?
    UserMailer.update(user.email, update).deliver_now!
  end
end

This concept comes with a slew of other concepts and techniques for working around it: nil?, present?, try, and now even a special syntax (&.).

This concept may be worth its cost in some situations, but can we sometimes do without it?

In our email case, how different is a nullable email from an array of emails?

User.new(emails: ["user@example.com"]).email
=> ["user@example.com"]

User.new(email: []).email
=> []

users.each do |user|
  user.emails.each do |email|
    UserMailer.update(user.email, update).deliver_now!
  end
end

Our usage patterns are very similar, but the second example removes an abstraction entirely. Now we don’t worry about arrays and nils; we only worry about arrays.

This works because nil is a specialization of Array. An Array represents a situation where there is an unknown number of something. A nil represents a situation where there could either be one or zero of something.

Sometimes nil may be just the right abstraction, but there are many ways to represent an empty case. A signed out user can be nil or an instance of a Guest class or an unsaved instance of your normal User class. Which introduces the best balance of low abstraction and low confusion for your application?

Conditionals

Every conditional is a special case the users of a codebase will need to parse.

We can use more common abstractions to avoid them:

# Conditional
users.each do |user|
  unless user.email.nil?
    UserMailer.update(user.email, update).deliver_now!
  end
end

# Abstraction
users.map(&:email).compact.each do |email|
  UserMailer.update(email, update).deliver_now!
end

Using a Null Object, we can introduce another abstraction to eliminate many special cases:

class Guest
  def admin?
    false
  end

  def can_edit?(post)
    false
  end

  def name
    "Guest"
  end
end

With this class in place, views will no longer need to check whether a user is signed in. We can decide how guests behave in one location and reuse that abstraction.

Folds

Many computations can be performed using an abstraction called “folding,” represented by the reduce (aka inject) method in Ruby:

# Using special cases
class Game
  def score
    result = 0
    rounds.each { |round| result += round.score }
    result
  end

  def competitors
    result = []
    rounds.each do |round|
      round.users.each do |user|
        result << user.email
      end
    end
    result
  end
end

# Using abstraction
class Game
  def score
    rounds.reduce(0) { |result, round| result + round.score }
  end

  def competitors
    rounds.reduce([]) { |result, round| result + round.users.map(&:email) }
  end
end

In this case, there is another level of abstraction built on top of a fold:

class Game
  def score
    rounds.sum(&:score)
  end

  def competitors
    rounds.map(&:users).flat_map(&:email)
  end
end

Using sum and flat_map introduce a specialization of a fold. In this case, which do you think is easier to understand?

Wrapping Up

Special cases aren’t special enough to break the rules.

— The Zen of Python

Almost everything you use in programming is an abstraction, but the trick is to decide how abstract you want to be. When coding try making your programs more abstract to see if you can eliminate special cases. Is it harder to understand without the clarity of specialization, or is it easier to follow because it uses a single level of abstraction?