Type Classes and Subtyping Two Different Approaches of Polymorphism
Total Page:16
File Type:pdf, Size:1020Kb
Type Classes and Subtyping Two different Approaches of Polymorphism Lars Hupel June 20, 2011 Abstract Traditionally, object oriented and functional programming languages have very few in common. While most of the former practise inheritance of classes, therefore introducing a concept of “subtyping”, the latter usually have algebraic data types without substitutability. However, Haskell’s type classes serve a similar purpose as (Java’s) inheritance: polymor- phism, a mechanism to allow functions to act differently depending on the type of values they act upon. This article will introduce these two seemingly contrary concepts and shows how to unify them. 1 Introduction Before talking about type classes and subtyping, we will first explore the mean- ing of polymorphism with respect to object-oriented programming languages. There are two main types of OOP languages, class-based and prototype-based, with JavaScript being the most famous of the few incarnations of the latter category. There may also be polymorphism in this category, but this kind is dif- ferent as it is mostly a consequence of the dynamic nature of these languages and therefore represents a different approach. Hence, we will restrict the following consideration to polymorphism in class-based, statically-typed OOP languages. Java For example, consider the object system of Java. Java defines classes as the main structure to organize data. If one class should inherit from another one, this has to be declared by using the extends keyword. Once extended, a fixed inheritance hierarchy is established, such that every time an instance of the superclass is expected, an instance of the subclass may be used instead. Java only allows to inherit from one class with Object being the head of the hierarchy. (The native types, e. g. int and float, exist independently. Values of those types are not objects and are thus treated differently.) It is however possible to implement syntactical interfaces which do not carry any method bodies. This restriction has been established, because it avoids the diamond problem known from other languages (as in C++, described below). Subclasses may (except for a couple of cases) override implementations of meth- ods, that is, supply a different method body for methods which already have a 1 body in one of the superclasses. Calls to methods are then dispatched dynam- ically, meaning that the lowermost (with respect to the inheritance hierarchy) defined body of this method is invoked. C++ One popular language allowing inheritance from more than one class is C++. The language has no distinction between interfaces and “regular” classes. Also, calls are statically dispatched, except when the virtual mod- ifier is present. Also, C++ has no head class of the hierarchy1. Suppose there is a hierarchy consisting of the four classes A, B, C and D with B and C inheriting from A and D inheriting from B and C. Furthermore, all classes except D implement a method f. When calling f on an instance of D, the compiler is unable to determine whether B::f or C::f should be invoked (the so called diamond problem) [4, 15, §15.2]. OCaml This functional language uses a different approach to subtyping, as this relation does not have to be declared explicitly. Classes are only used as primitive templates for objects, such that an object is typed by the public methods it exposes2. Whether two values are substitutable is decided based on the compatibility of the signature. Inheritance for classes only means that the methods of the superclass are copied into the subclass [1]. Inheritance vs. Subtyping We can conclude that inheritance and subtyping are two related, but separate issues. This distinction is clear for languages with structural subtyping (as OCaml) and not so clear for languages with nominal subtyping (as Java and C++). There are also languages with a mixture between both (as Scala). 2 Subtyping 2.1 Concepts Definition In general, subtyping means the substitutability of values of an supertype by values of a subtype. In that case, some sort of “hierarchy” is introduced which does not necessarily have to be a tree. It is clear though that it induces a relation S <: T, where S is a subtype of T. A good example for that is a hierarchy of number types, where Number is the supertype of e. g. Integer, Float and Complex. Every algorithm which takes a Number as an input is therefore able to take an Integer instead. There is a formalization of this model, called the Liskov substitution principle (LSP), as stated by Barbara Liskov and Jeanette Wing as following: 1There is a special pointer-to-void type, void*, though, classifying a pointer to a value of unknown type. 2In fact, a signature is written as < name : type ; .. > where the ellipsis stands for arbitrary methods. Every object exposing only the specified methods – regardless of its other methods – may be passed where this type is expected. 2 “Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.” [6, p. 1] The LSP is important in deciding whether a type should be modelled as a subtype of another type. For example, consider the two types Square and Rectangle. From a mathematical point of view, a square is clearly a rectangle with the additional property that all sides have the same length. However, if we set φ to “may be scaled independently horizontally and vertically”, the LSP would not hold for Square <: Rectangle, because φ is true for rectangles but not for squares. For the opposite direction assume φ0 is “can be described by only one length”. φ0 is true for squares but not for rectangles which leaves us with the conclusion that Rectangle <: Square does not hold either. Note that the set of properties which should be satisfied by a given hierarchy depends on the requirements. In a context where immutability is desired, it would be fine to define Square as a subtype of Rectangle, because φ is no longer a property of rectangles. Classes and inheritance The subtype relation does not imply anything about the actual implementation of two types S <: T. Speaking about Java, S and T denote interfaces without implementing classes involved. In contrast, the concept of a class entails an implementation which conforms to the interface. More generally speaking, an object is a collection of fields. Every field denotes an attribute (value, property) or a method (procedure, behaviour). In pure OOP languages, everything is an object, even “primitive” types which have a correspondence on hardware like floating point numbers or integers. As we are just regarding class-based languages, every object is an instance of a class where the class serves as a template and a mechanism to construct new instances. As seen earlier, inheritance and subtyping are different matters. In Java, a class is coupled with a type which makes this distinction a bit opaque. Subtyping, as defined by Liskov, works on a semantical level, whereas Java only requires a class to implement the methods from every used interface. For example, consider an interface for sorting values: interface Sort { /** * Sorts an array. * The input array is not modified. * @return an array with the same values as the * input but in ascending order */ int[] sort(int[] input); } Nothing prevents a class implementing Sort and ignoring the “contract” of sort, for instance by mutating the input array. With respect to behavioural subtyping, 3 such a “malicious” class would not be a subtype of Sort. Obviously, checking such a contract is a particularly hard task as it requires the definition of pre- and postconditions and a tool which checks the compliance at compile or run time. Such approaches have been made (e. g. [2]), but the vast majority simply uses Javadoc comments as shown above. Coming back to our example, modelling Square <: Rectangle. Assume both types are represented by classes. Then, Rectangle may have two fields for both lengths. If Square inherits from Rectangle, it also gets these fields although a square is defined by only one length. It is no problem for Square to keep both lengths synchronized, but such redundancies are generally undesired and should be taken into consideration when modelling class hierarchies. Variance A source of never ending joy (and confusion) are the consequences of variance with respect to both subtyping and generic types. Assume that A <: B and C <: D and the two function types τ1 = B -> C and τ2 = A -> D. Let φ1 be the property “may take any instance of A” and φ2 “returns an instance of D”. Then, of course φ2 holds for both τ1 and τ2, because all instances of C are also instances of D. φ1 trivially holds for τ2, but also for τ1 as the set of instances of A is a subset of the set of instances of B. Thus, we have shown that τ1 <: τ2 [17, p. 6]. These properties are important when the signature of a method in a subtype is different from the signature of the corresponding method in the supertype. The parameter types of a method are contravariant, whereas the return type is covariant [6, p. 10, 18, §2]. In conclusion, there are three possibilities of variance where a type appears in a particular position: covariant iff S <: T, S may be used instead of T, thus preserving the original order contravariant iff S <: T, T may be used instead of S, reversing the subtyping order invariant neither covariance or contravariance applies The concept of variance may be generalized, which immediately leads to generic programming. The variance of A :> B is just a special case, as most OOP languages allow classes to be parametrized on types.