I know that there is a similar question here, but I would like to see an example, which clearly shows, what you can not do with interface
and can with Type Class
For comparison I'll give you an example code:
class Eq a where
(==) :: a -> a -> Bool
instance Eq Integer where
x == y = x `integerEq` y
C# code:
interface Eq<T> { bool Equal(T elem); }
public class Integer : Eq<int>
{
public bool Equal(int elem)
{
return _elem == elem;
}
}
Correct my example, if not correctly understood
Others have already provided excellent answers.
I only want to add a practical example about their differences. Suppose we want to model a "vector space" typeclass/interface, which contains the common operations of 2D, 3D, etc. vectors.
In Haskell:
In C#, we might try the following wrong approach (I hope I get the syntax right)
We have two issues here.
First,
scale
returns only anIVector
, a supertype of the actualVec2D
. This is bad, because scaling does not preserve the type information.Second,
add
is ill-typed! We can't usev.x
sincev
is an arbitraryIVector
which might not have thex
field.Indeed, the interface itself is wrong: the
add
method promises that any vector must be summable with any other vector, so we must be able to sum 2D and 3D vectors, which is nonsense.The usual solution is to switch to F-bounded quantification AKA CRTP or whatever it's being called these days:
The first time a programmer meets this, they are usually puzzled by the seemingly "recursive" line
Vec2D : IVector<Vec2D>
. I surely was :) Then we get used to this and accept it as an idiomatic solution.Type classes arguably have a nicer solution here.
After a long study of this issue, I came to an easy method of explaining. At least for me it's clear.
Imagine we have method with signature like this
And implementation of
IComparator
:Then we can write code like this:
We can improve this code, first we create
Dictionary<Type, IComparator>
and fill it:And after this let's redesign
Sort
method signatureAs you understand we are going to inject
IComparator<T>
, and write code like this:As you already guessed this code will not work for other types until we outline the implementation and add in
Dictionary<Type, IComparator>
And now imagine if this work was done for us by the compiler during build and it threw exception if it could not find the comparator with corresponding types.
For this, we could help the compiler and add a new keyword instead of usage attribute. Out
Sort
method will be look like this:And code of realization concrete Comparator:
And give the name to this style of implementation Type Class
I hope that I understood correctly and understandably explained.
PS: The code does not pretend to work.
A C# interface defines a set of methods that must be implemented. A Haskell type class defines a set of methods that must be implemented (and possibly a set of default implementations for some of the methods). So there's a lot of similarities there.
(I guess an important difference is that in C#, an interface is a type, whereas Haskell regards types and type classes as strictly separate things.)
The key difference is that in C#, when you define a type (i.e., write a class), you define exactly what interfaces it implements, and this is frozen for all time. In Haskell, you can add new interfaces to an existing type at any time.
For example, if I write a new
SerializeToXml
interface in C#, I cannot then makedouble
orString
implement that interface. But in Haskell, I can define my newSerializeToXml
type class, and then make all the standard, built-in types implement that interface (Bool
,Double
,Int
...)The other thing is how polymorphism works in Haskell. In an OO language, you dispatch on the type of the method the object is being invoked on. In Haskell, the type that the method is implemented for can appear anywhere in the type signature. Most particularly,
read
dispatches on the return type you want — something you usually can't do at all in OO languages, not even with function overloading.Also, in C# it's kind of hard to say "these two arguments must have the same type". Then again, OO is predicated on the Liskov substitution principal; two classes that both descend from
Customer
should be interchangeable, so why would you want to constrain twoCustomer
objects to both be the same type of customer?Come to think of it, OO languages do method lookup at run-time, whereas Haskell does method lookup at compile-time. This isn't immediately obvious, but Haskell polymorphism actually works more like C++ templates than usual OO polymorphism. (But that's not especially to do with type classes, it's just how Haskell does polymorphism as such.)
Typeclasses are resolved based on a type, while interface dispatch happens against an explicit receiver object. Type class arguments are implicitly provided to a function while objects in C# are provided explicitly. As an example, you could write the following Haskell function which uses the
Read
class:which you can then use as:
and have the appropriate
read
instance provided by the compiler.You could try to emulate the
Read
class in C# with an interface e.g.but then the implementation of
ReadLine
would need a parameter for theRead<T>
'instance' you want:The
Eq
typeclass requires both arguments have the same type, whereas yourEq
interface does not since the first argument is implicitly the type of the receiver. You could for example have:which you cannot represent using
Eq
. Interfaces hide the type of the receiver and hence the type of one of the arguments, which can cause problems. Imagine you have a typeclass and interface for an immutable heap datastructure:Merging two binary heaps can be done in O(n) while merging two binomial heaps is possible in O(n log n) and for fibonacci heaps it's O(1). Implementors of the Heap interface do not know the real type of the other heap so is forced to either use a sub-optimal algorithm or use dynamic type checks to discover it. In contrast, types implementing the
Heap
typeclass do know the representation.