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 theBurger
class. No changes are needed inDiner
. - 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!