CSC/ECE 517 Fall 2007/wiki1 5 47
What is duck typing?
“If it walks like a duck and quacks like a duck, it must be a duck” is the philosophy of duck typing.
In terms of Ruby, duck typing refers to the tendency to be less concerned with the class of an object and more concerned with what methods can be called on it and what operations can be performed on it. Duck typing allows an object to be passed in to a method that expects a certain type even if it doesn’t inherit from that type. All it has to do is support the methods and properties of the expected type in use by the method.
Consider that we have two objects Circle and Square. These objects are clearly different from each other and clearly do not belong to the same class. Assume that they have some common methods like Area() and Perimeter(). Although these methods have the same names, their implementations are completely different. Now assume that we have a method DisplayDetails() which calls the methods Area() and Perimeter(). If we explicitly specify a type for the argument of DisplayDetails(), we will need two separate functions to display the area and perimeter of a Circle and a Square. But by using duck typing, we take off the argument type of DisplayDetails(), thereby enabling us to pass objects of either type and have their methods invoked.
Let’s see a small example now of how duck typing is implemented in Ruby.
def get_successor(s) raise ArgumentError, 'No successor method!' unless s.respond_to? :succ return "Successor of #{s} is #{s.succ}" end puts get_successor('a') => Successor of a is b puts get_successor(4) => Successor of 4 is 5
In this example, we define a method ‘get_successor’ which returns the successor of the object passed to it. All the objects on which the ‘succ’ method is defined can be passed as an argument to the ‘get_successor’ method irrespective of their class.
Duck typing does not rely on inheritance and polymorphism. Every single object is accepted in every single method. The only thing that matters is the capability of that object to do whatever the method is trying to do. If the object is unable to do that, the interpreter happily raises an exception.
Duck typing v/s Interfaces and Inheritance
Advantages of Duck typing over Interfaces:
Consider the following example, in which the salary of an employee is calculated depending on his post and the number of days he has worked in the month. We will contrast the approaches of duck typing (Ruby) and interface (Java) one at a time.
Java Code:
package duck_typing; interface getSalary { public void compute(int days); } class Manager implements getSalary { public void compute(int days) { System.out.print("This manager has worked " + days + " days this month, "); System.out.println("so his salary is $" + (double) days*150); } } class Clerk implements getSalary { public void compute(int days) { System.out.print("This clerk has worked " + days + " days this month, "); System.out.println("so his salary is $" + (double) days*55); } } public class Calculate { public static void main(String[] args) { getSalary[] emp = {new Manager(), new Clerk()}; for (int i = 0; i < emp.length; i++) { emp[i].compute(20); } } }
Output:
This manager has worked 20 days this month, so his salary is $3000
This clerk has worked 20 days this month, so his salary is $1100
In the above code, we have created an interface ‘getSalary’ which has the abstract method ‘compute’. The two classes Manager and Clerk need to implement this interface and define the method ‘compute’, to calculate the salaries of its objects. As can be seen from the code, we can compute the salaries of both a manager as well as a clerk, even though they belong to different classes.
Now let’s see how the same can be implemented in Ruby using duck typing.
====Ruby code:====
class Manager def compute(days) print "This manager has worked #{days} days this month, " puts "so his salary is $#{days * 150}" end end class Clerk def compute(days) print "This clerk has worked #{days} days this month, " puts "so his salary is $#{days * 55}" end end emp = [Manager.new(), Clerk.new()] emp.each {|em| puts em.compute(20) }
Output:
This manager has worked 20 days this month, so his salary is $3000
This clerk has worked 20 days this month, so his salary is $1100
It can clearly be seen that implementing the above code is much simpler and less verbose as compared to java. In Ruby, we just created two classes, Manager and Clerk which implemented the method ‘compute’. We were able to invoke the ‘compute’ method with the objects of different classes.
Thus it can be said that duck typing is a big time saver when we write codes, as the verification that the object does respond to different methods is not made when the object is passed as an argument to the method, but is made on the invocation of the said methods. And this is the reason why arguments to methods are usually not typed all.
We can sum up the advantages of duck typing as follows: •Granularity: Duck-typed functions are the least dependent on the types of the arguments passed. On the contrary, interfaces may carry extra requirements, like some methods that are not required for the function in question. Although we can split the interfaces by keeping only a certain number of required functions in them, the resulting increase in the number of interfaces can cause confusion. •Adaptability: Thanks to the independence of duck-typed functions from the type of arguments passed, a duck-typed function can be adapted to contexts that it was not built for initially, with a minimal effort on the part of the caller. •Sheer convenience: Since the types of arguments of methods are not typed, it eliminates the need to write separate caller functions based on the type of arguments to be passed.
Disadvantages of Duck typing over interfaces:
The first drawback of duck-typing is the lack of compiler safety net. If enough care is not taken while writing a piece of code, there is little guarantee that it will run successfully. The second drawback is the lack of a precise contract between the calling & the called methods. The only way to find it out is to analyze the body of the called method, which can be impractical at times, especially when we deal with a third-party API. The most common approach to mitigate the issues caused by this is to provide documentation. But even that is not really advisable, as the documentation can get too verbose and less direct (informative). Also, there is a serious likelihood that it may not be in sync with the code, as the code evolves with time.