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. :)
Functor
in OOP? – Sacring