What is the difference between type class dependence in haskell and sub typing in OOP?
Asked Answered
C

1

8

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?

Cornemuse answered 27/6, 2018 at 5:49 Comment(1)
I don't think it's a question of what sub-typing can do that type classes can't, but rather the other way around. Haskell's type classes can express relationships that I wouldn't know how to do with OOP, although that'd clearly be language-dependent. How would you, for example, define Functor in OOP?Sacring
V
18

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 Monads are Applicatives, and all Applicatives are Functors.

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:

  1. 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)

  2. 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. :)

Viscoid answered 27/6, 2018 at 7:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.