The previous quote was not the first invocation of the duck test but it is the most famous. The usage of this test may date back to the early 20th century as a way of arguing that “given the properties I can observe about some object, I’m going to classify it as what it appears to be.” It is sometimes deployed as a counterargument to claims that something is not what it seems.
The question before the house today is this: Is go duck-typed? Other languages claim to be so (see below), and the go team has mentioned in public presentations an affinity for this particular type strategy.
In programming languages, duck typing can be roughly approximated as the application of the duck test to objects. To increase the precision slightly, a type is defined to be a collection of things that the type can do. Let’s try a go-ish example:
Thus, we will define some type to be an
Animal if it can return the number of legs it has. The previous is an example of an interface in go, so it implies that there are one or more implementations. An interface in go is a “test” in the sense of the duck test above; this type declaration is an animal test.
Given the animal test, let’s define a whole zoo!
Given the current
Animal interface definition, these are all duck-typed as
Surely, one cannot have intended a three-legged stool to be an animal?!? Well, if one applies the—admittedly weak—animal test that we constructed with the interface
Animal above, the stool certainly passes. The critical idea of duck typing is “can I use the object in question in the way I want” and if so, the object passes the test. Clearly we can improve our definition of the animal test in this way:
With this better version of
Animal we can (correctly?) disallow a stool to be considered an animal because it has no
Genus() method. This example may seem contrived but by perusing the go standard library one will notice that a great many of the go standard interfaces have one or two methods. In fact,with a small program, one can compute the histogram of the number of methods that occur in interface declarations in the go standard library.
The histogram above indicates that number of interfaces with one or two methods is greater than three times all the others combined!
Duck Typing and Comparative Programming Languages
The notion of a “type” in a programming language goes back to the origin of programming languages. The arguing about what is a “type” in programming languages goes back to five minutes after the origin of programming languages. Many Ph.D. theses have been written about these arguments, entire conferences are dedicated to the subject, and books are written in an attempt to formally define a type (for example, A Theory of Objects or the Z notation).
For the purpose of simplicity we will define an interface to be a type that specifies a contract about how an instance of the type will perform his duties. One could say that the first widely known programming language with the “object must meet the contract” paradigm was Eiffel in 1985, although this is arguable. Given our (simplistic) definition of an interface/contract how do programming languages differ? We’ll divide languages into two categories: those that require the specification of the interface and those that do not.
The most common reason for requiring that an interface be defined is performance, although there are others. The general reason that specifying interfaces provide additional performance is that a traditional compiler or a Just-In-Time (JIT) compiler can make assumptions about the underlying memory layout so long as the specification of the interface is enforced by the language. To use our improved example above with
Animal, a hypothetical compiler could assume that the method
NumberOfLegs is 0 bytes from the memory address of the instance implementing
Animal and the method
Genus is located 8 bytes from the memory address of the instance. So long as said compiler also enforces this requirement when compiling the implementation of
Dog, the code that accepts an
Animal can 1) not need to know the true type of the instance passed as a parameter and 2) jump (branch) directly to the memory address of the implementation of
Genus for fast performance. Today, many compiled, object-oriented languages from Java (1991) to C# (2000) and from Rust (2010) to Swift (2014) operate with these explicit interfaces (this list is far from exhaustive!).
The other alternative is, well, to just wait. This is the case that is usually considered to be duck typing. In this formulation, you simply pass any type as a parameter to any function or method and wait to see if the parameter is used in a way that is acceptable. So, if type
Spider is passed to method
Anatomy(a) as the parameter
a, there is no check to see if
Spider will work in all code paths inside
Anatomy. We simply try to run the program and see if the method calls on
Spider) work out. If they don’t, we generate an error and abort
Anatomy. You can easily imagine a dumb definition of
Anatomy() which only calls
NumberOfLegs() and since that is allowed on
Spider as a parameter is ok. Notably, the use of
Stool as parameter a would be ok too! In this model, the “contract”–if one can be said to exist–is implicit and varies with the exact code path that is taken through the function
Anatomy. Languages like Python (1990) and Ruby (1995) operate in this fashion; the Python community popularized the phrase “duck typing” for this behavior.
Duck Typing and Go
Go is an unusual hybrid of these two ideas. Let’s think about an example: Joe has defined and has exported this package
Joe compiles this code and makes it available to his team. A week later, Mary writes this code:
If Mary runs her code from
main() she’ll see “dwarf is sad” and then on the next line “dwarf is happy.”
Given our poor man’s definition of duck typing earlier, when the language waits to see how the type is used, which case are we in here?
Does go have duck typing or not?
One can make the case that in the small we are in the first case explained above: the interface has to be explicitly defined. Within the code written by Mary, it seems fairly clear that the interface
IsHappy is defined so that it can be used to type check the uses of the parameter
PrintHappy. As we explained previously,
IsHappy is being used to accelerate the call to
In the large though, one can easily argue that we are in the duck typing case above. Joe’s code didn’t know or care about Mary’s code, and since
g in Mary’s code worked because it had the proper method for
Happy() that is used in (Mary’s)
PrintHappy() that effectively we just waited to see if Grumpy could legitimately be used successfully in
PrintHappy. What do we conclude from this? Sometimes, you have to embrace the ambiguity!
You Might Also Like
Why you should be using errgroup.WithContext() in your Golang server handlers