Programmers’ interactions with objects are influenced by the fundamental Ruby concept of duck typing, which puts behavior above formal class structure.
Explanation of Duck Typing in Ruby
Duck typing in Ruby is a programming concept where the type or class of an object is less important than the methods it defines. It emphasizes behavior over inheritance, meaning that if an object can respond to a specific method, it is treated as having the necessary “type” for that operation, regardless of its actual class. The idea that if something “walks like a duck and quacks like a duck, it must be a duck” is known as dragon typing. This implies that rather than the class from which an object derives or the modules it contains, its usefulness is based on what it can accomplish (the methods it supports).
The capabilities of an object determine its type in Ruby, a dynamically typed language.
Comparison to Strong Typing
Ruby’s dynamic typing allows the interpreter to treat an object as a “duck” if it “walks like a duck and talks like a duck,” unlike tightly typed languages (like C# or Java) where a variable specified as an array cannot be utilized as another data type.
- Strong Typing usually requires that an object provided to a method be an instance of an expected class or interface, which is known as type checking and is frequently done at compile time.
- Duck Typing makes typing relevant to a task mandatory. The code functions properly if a method calls for an object that supports that method (such as quack), provided that the object, regardless of its class, reacts to the method.
Beginners may put off learning the notions of explicit types or classes until they are absolutely required, but this method is quite convenient for them.
The Role of Duck Typing
Instead of using is_a?
to check a variable’s class when programming with the duck typing philosophy, it is better to use the respond_to?
method to see if the variable supports a certain method. By depending on respond_to?
, you guarantee that you and other future users can design new classes that provide the same functionality without being constrained by an existing class structure.
Writing a method that only accepts Strings instead of anything that can be converted (like to_str
) compromises the duck typing technique for other users of your code.
Code Examples Demonstrating Duck Typing
The following examples show how Duck Typing enables generic treatment of various objects as long as they share the necessary behavior (method signature).
Example 1: Objects with Shared Behavior
Think of Duck
, Goose
, and DuckRecording
as three classes.
class Duck
def quack
'Quack!'
end
def swim
'Paddle paddle paddle...'
end
end
class Goose
def honk
'Honk!'
end
def swim
'Splash splash splash...'
end
end
class DuckRecording
def quack
play
end
def play
'Quack!'
end
end
# --- ADDED CODE TO CREATE OUTPUT ---
# 1. Instantiate the classes
daisy = Duck.new
greg = Goose.new
tape = DuckRecording.new
puts "--- Method Calls ---"
# 2. Call methods on the objects
puts "Daisy the Duck says: #{daisy.quack}" # Duck responds to quack
puts "Daisy the Duck swims: #{daisy.swim}"
puts "Greg the Goose says: #{greg.honk}" # Goose responds to honk
puts "Greg the Goose swims: #{greg.swim}"
# 3. Demonstrate Duck Typing (Focusing on the 'quack' method)
# Even though DuckRecording is not a Duck, it has a 'quack' method.
puts "\n--- Duck Typing Example ---"
def make_it_quack(animal)
# This method doesn't care about the object's class, only that it responds to 'quack'
puts "I asked an object to quack, and it said: #{animal.quack}"
end
puts "Asking the Duck to quack:"
make_it_quack(daisy) # Works!
puts "Asking the DuckRecording to quack:"
make_it_quack(tape) # Works, demonstrating Duck Typing!
Output
--- Method Calls ---
Daisy the Duck says: Quack!
Daisy the Duck swims: Paddle paddle paddle...
Greg the Goose says: Honk!
Greg the Goose swims: Splash splash splash...
--- Duck Typing Example ---
Asking the Duck to quack:
I asked an object to quack, and it said: Quack!
Asking the DuckRecording to quack:
I asked an object to quack, and it said: Quack!
If the expected method is present, any of these classes can be handled by methods written in Ruby that make use of duck typing:
class Duck
def quack
'Quack!'
end
def swim
'Paddle paddle paddle...'
end
end
class Goose
def honk
'Honk!'
end
def swim
'Splash splash splash...'
end
end
class DuckRecording
def quack
play
end
def play
'Quack!'
end
# DuckRecording does NOT have a swim method
end
# --- Functions Demonstrating Duck Typing ---
def make_it_quack(duck)
# Relies only on the presence of the 'quack' method
duck.quack
end
def make_it_swim(animal)
# Relies only on the presence of the 'swim' method
animal.swim
end
# --- Usage (Output) ---
puts "--- Quack Examples ---"
# Works for Duck
puts make_it_quack(Duck.new) # => Quack!
# Works for DuckRecording (even though it's not a Duck)
puts make_it_quack(DuckRecording.new) # => Quack!
puts "\n--- Swim Examples ---"
# Works for Duck
puts make_it_swim(Duck.new) # => Paddle paddle paddle...
# Works for Goose (even though it's not a Duck)
puts make_it_swim(Goose.new) # => Splash splash splash...
# Note: Calling make_it_swim(DuckRecording.new) would raise a NoMethodError
# because DuckRecording does not respond to the 'swim' method.
Output
--- Quack Examples ---
Quack!
Quack!
--- Swim Examples ---
Paddle paddle paddle...
Splash splash splash...
Nevertheless, a NoMethodError
will be raised if an object is supplied that does not define the desired method:
make_it_quack(Goose.new) # NoMethodError: undefined method `quack' for #<Goose:0x...>
Example 2: Using for Robustness
When drafting a method that must guarantee that its input is “point-like” (has x
and y
methods), using respond_to?
to check for the capability offers flexibility and, if required, an enhanced error message:
# A simple struct-like class to hold point coordinates
Point = Struct.new(:x, :y) do
# Override the standard addition operator
def +(other)
# 1. Looser version of type checking using respond_to?
raise TypeError, "Point-like argument expected" unless
other.respond_to?(:x) && other.respond_to?(:y) # Check for methods
# 2. Perform the addition using the 'x' and 'y' methods
Point.new(self.x + other.x, self.y + other.y)
end
end
# --- ADDED CODE TO CREATE OUTPUT ---
# Create Point objects
p1 = Point.new(10, 20)
p2 = Point.new(5, -3)
# 1. Demonstrate Point + Point (Successful Operation)
p3 = p1 + p2
puts "Point 1: #{p1.inspect}" # => #<struct Point x=10, y=20>
puts "Point 2: #{p2.inspect}" # => #<struct Point x=5, y=-3>
puts "Point 1 + Point 2: #{p3.inspect}"
# Expected Output: Point.new(10+5, 20-3) => #<struct Point x=15, y=17>
puts "\n"
# 2. Demonstrate Duck Typing (Successful Operation with a non-Point object)
class DummyVector
# A class that isn't a Point, but implements the required 'x' and 'y' methods
def x;
100;
end
def y;
200;
end
end
dummy = DummyVector.new
p4 = p1 + dummy # This works because dummy.respond_to?(:x) and dummy.respond_to?(:y) are true
puts "Point 1 + DummyVector: #{p4.inspect}"
# Expected Output: Point.new(10+100, 20+200) => #<struct Point x=110, y=220>
puts "\n"
# 3. Demonstrate Failure (Raises TypeError due to missing method)
begin
p1 + "not a point" # Strings don't respond to 'x' or 'y'
rescue TypeError => e
puts "Error caught: #{e.message}"
end
Output
Point 1: #<struct Point x=10, y=20>
Point 2: #<struct Point x=5, y=-3>
Point 1 + Point 2: #<struct Point x=15, y=17>
Point 1 + DummyVector: #<struct Point x=110, y=220>
Error caught: Point-like argument expected
This preserves the duck type concept by guaranteeing that any object which defines methods called x
and y
will be appropriate for the +
operation, regardless of its class.
Example 3: File-like Objects
For operations like file I/O and testing, duck typing is useful. The Enumerable module is part of the IO class and the String class. Often, you can write code to read from an object without knowing its type (e.g., String
or File
). A file object or a StringIO
object (which emulates a file in memory) might be passed to a helper method that expects to “write” data to an object, for example.
Duck typing can also be used for resource management in setup and cleanup tasks, such as making sure a file is closed, by employing a method that expects an object that responds to close
(such as File.new
utilizing a block).
You can also read What Is Code Blocks In Ruby With Practical Code Examples