We often use type class dependence to emulate the sub typing relationship.
e.g:
when we want to express the sub typing relationship between Animal, Reptile and Aves in OOP:
abstract class Animal {
abstract Animal move();
abstract Animal hunt();
abstract Animal sleep();
}
abstract class Reptile extends Animal {
abstract Reptile crawl();
}
abstract class Aves extends Animal {
abstract Aves fly();
}
we can translate each abstract class above into a type class in Haskell:
class Animal a where
move :: a -> a
hunt :: a -> a
sleep :: a -> a
class Animal a => Reptile a where
crawl :: a -> a
class Animal a => Aves a where
fly :: a -> a
And even when we want a heterogeneous list, we have ExistentialQuantification .
So I'm wondering, why we still say that Haskell doesn't have sub-typing, is there still something which sub-typing can do but type class cannot? What is the relationship and difference between them?
What's a typeclass in Haskell? A typeclass defines a set of methods that is shared across multiple types. For a type to belong to a typeclass, it needs to implement the methods of that typeclass. These implementations are ad-hoc: methods can have different implementations for different types.
In Haskell, type classes provide a structured way to control ad hoc polymorphism, or overloading. [For the stylistic reason we discussed in Section 3.1, we have chosen to define elem in infix form. == and || are the infix operators for equality and logical or, respectively.]
The class defines object's internal state and the implementation of its operations. In contrast, an object's type only refers to its interface - a set of requests to which it can respond. An object can have many types, and objects of different classes can have the same type.
A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes. A lot of people coming from OOP get confused by typeclasses because they think they are like classes in object oriented languages.
A typeclass with one parameter is a class of types, which you can think of as a set of types. If Sub
is a subclass (sub-typeclass) of Super
, then the set of types implementing Sub
is a subset of (or equal to) the set of types implementing Super
. All Monad
s are Applicative
s, and all Applicative
s are Functor
s.
Everything you can do with subclassing, you can do with existentially quantified, typeclass-constrained types in Haskell. This is because they’re essentially the same thing: in a typical OOP language, every object with virtual methods includes a vtable pointer, which is the same as the “dictionary” pointer that’s stored in an existentially quantified value with a typeclass constraint. Vtables are existentials! When someone gives you a superclass reference, you don’t know whether it’s an instance of the superclass or a subclass, you only know that it has a certain interface (either from the class or from an OOP “interface”).
In fact you can do more with Haskell’s generalised existentials. An example I like is packing an action returning a value of some type a
along with a variable where the result will be written once the action completes; the source returns a value of the same type as the variable, but this is hidden from the outside:
data Request = forall a. Request (IO a) (MVar a)
Because Request
hides the type a
, you can store multiple requests of different types in the same container. Because a
is completely opaque, the only thing that a caller can do with a Request
is run the action (synchronously or asynchronously) and write the result into the MVar
. It’s hard to use it wrong!
The difference is that in OOP languages you can typically:
Implicitly upcast—use a subclass reference where a superclass reference is expected, which must be done explicitly in Haskell (e.g. by packing in an existential)
Attempt to downcast, which is not allowed in Haskell unless you add an extra Typeable
constraint that stores the runtime type information
Typeclasses can model more things than OOP interfaces and subclassing, however, for a few reasons. For one thing, since they’re constraints on types, not objects, you can have constants associated with a type, such as mempty
in the Monoid
typeclass:
class Semigroup m where
(<>) :: m -> m -> m
class (Semigroup m) => Monoid m where
mempty :: m
In OOP languages there’s typically no notion of a “static interface” that would let you express this. The future “concepts” feature in C++ is the nearest equivalent.
The other thing is that subtyping and interfaces are predicated on a single type, whereas you can have a typeclass with multiple parameters, which denotes a set of tuples of types. You can think of this as a relation. For example, the set of pairs of types where one can be coerced to the other:
class Coercible a b where
coerce :: a -> b
With functional dependencies, you can inform the compiler of various properties of this relation:
class Ref ref m | ref -> m where
new :: a -> m (ref a)
get :: ref a -> m a
put :: ref a -> a -> m ()
instance Ref IORef IO where
new = newIORef
get = readIORef
put = writeIORef
Here the compiler knows that the relation is single-valued, or a function: each value of the “input” (ref
) maps to exactly one value of the “output” (m
). In other words, if the ref
parameter of a Ref
constraint is determined to be IORef
, then the m
parameter must be IO
—you cannot have this functional dependency and also a separate instance mapping IORef
to a different monad, like instance Ref IORef DifferentIO
. This type of functional relation between types can also be expressed with associated types or the more modern type families (which are usually clearer, in my opinion).
Of course, it’s not idiomatic to translate an OOP subclass hierarchy directly to Haskell using the “existential typeclass antipattern”, which is usually overkill. There’s often a far simpler translation, such as ADTs/GADTs/records/functions—roughly this corresponds to the OOP advice of “prefer composition over inheritance”.
Most of the time, when you would write a class in OOP, in Haskell you shouldn’t generally reach for a typeclass, but rather a module. A module that exports a type and some functions operating on it is essentially the same thing as the public interface of a class, when it comes to encapsulation and code organisation. For dynamic behaviour, typically the best solution isn’t type-based dispatch; instead, just use a higher-order function. It is functional programming, after all. :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With