Is a hotdog a sandwich? Solving futile debates with duck typing

At a retreat my company held in New Orleans last year, we went out for some Cajun dinner. I don’t recall who, but someone at the table brought up the argument "is a hotdog a sandwich?". I was new to this debate so right off the bat I said, "No" and I felt quite confident in my answer. But 20 minutes later, after hearing everyone's arguments, I wasn't so sure anymore. What made a sandwich a sandwich? Is a hotdog perhaps a subtype of sandwich? Where do you draw the line between a "classic" sandwich, and any of its endless variations?

We never settled this. Really, it's a question without an answer, isn't it? And does it even matter? If I'm hungry for a handheld meal of some sort, and you pass me a sandwich or a hotdog, I'll eat it and probably be satisfied. I'm not too picky about food. Maybe you are, but the end result is the same - you'd have fed yourself with your hands and feel moderately to fully satiated.

In Ruby, and in any other dynamically typed languages like Python and JavaScript, there is a concept called duck typing. The name comes from the phrase "If it looks like a duck, swims like a duck, and quacks like a duck, then for all intents and purposes it is a duck.". This means that in Ruby, if an object implements certain methods, it can be treated as a specific type without actually being that type. Rather than focusing on what an object is (its class), Ruby cares more about what an object can do (its behavior).

For example, consider a method that expects an object obj with a speak method:

def make_it_speak(obj)
 obj.speak
end

In this case, make_it_speak will work with any object that has a speak method, regardless of its class. Whether it’s an instance of Person, Dog, or any other class doesn’t matter as long as it has a speak method. This flexibility allows Ruby code to be highly polymorphic and adaptable. A duck-typed object is one whose taxonomy is determined by its methods and properties rather than by class inheritance, included modules or any other explicit type definitions.

Why is this useful? Let’s imagine a scenario where we have a Diner class (like a person who is dining). This class is responsible for handling food items, specifically Hotdog and Sandwich. The diner’s job is to "hold" the food (grab it) and "chew" it. Each food item might have a slightly different way of being held or chewed, but as long as they implement hold and chew methods, the Diner can handle them without knowing what specific type of food it is.

Non-Duck Typed Example

In this non-duck-typed example, the Diner class explicitly checks whether the food is a Hotdog or a Sandwich before calling their respective methods. This tightly couples Diner to each specific food type, making it harder to introduce new foods without modifying the class.

class Diner
 def eat(food)
   case food
   when Hotdog
     hold_hotdog(food)
     chew_hotdog(food)
   when Sandwich
     hold_sandwich(food)
     chew_sandwich(food)
   else
     raise "Unknown food type"
   end
 end

 private

 def hold_hotdog(food)
   puts "Holding the hotdog by the bun."
 end

 def chew_hotdog(food)
   puts "Chewing the hotdog carefully."
 end

 def hold_sandwich(food)
   puts "Holding the sandwich with two hands."
 end

 def chew_sandwich(food)
   puts "Chewing the sandwich with small bites."
 end
end

Here, the Diner class has to know how to handle each specific food item, making it difficult to add new food types without constantly modifying Diner.

Duck-Typed Example

Using duck typing, we can instead expect each food item to implement hold and chew methods. This way, Diner can call these methods without caring if the food is a Hotdog, Sandwich, or any other item, as long as they respond to hold and chew.

First, we’ll define each food class with hold and chew methods:

class Hotdog
 def hold
   puts "Holding the hotdog by the bun."
 end

 def chew
   puts "Chewing the hotdog carefully."
 end
end

class Sandwich
 def hold
   puts "Holding the sandwich with two hands."
 end

 def chew
   puts "Chewing the sandwich with small bites."
 end
end

Now, Diner simply calls hold and chew without needing to know the specific type of food:

class Diner
 def eat(food)
   food.hold
   food.chew
 end
end

Using the Diner Class

With this setup, Diner can now handle any food item that implements hold and chew:

class MealService
 def feed(diner)
   diner.eat(Hotdog.new)
   diner.eat(Sandwich.new)
 end
end

There are a bunch of benefits in using duck typing here:

  • Eliminates Class Checks: The Diner class no longer checks for specific food types, resulting in simpler and more readable code.
  • Easier to Extend: Adding a new food item (e.g., Burger) only requires defining the hold and chew methods in the Burger class. No changes are needed in Diner.
  • Decouples Code: Each food type encapsulates its own behavior, and Diner simply trusts that each food item knows how to be held and chewed.

This approach is analogous to treating hotdogs as "sandwiches" simply because they have sandwich-like behaviors (i.e., they can be held and chewed similarly). This allows for a flexible design, where food items can be swapped out and added as long as they implement the expected methods.

So, after all this, what is the answer to the "is a hotdog a sandwich" question? With duck typing, the answer is: it doesn't matter! As long as I can hold it and chew it, it may as well be a sandwich but I don't care either way.

Bon appetite!