What are the differences between typeclasses and Abstract Data Types?
I realize this is a basic thing for Haskell programmers, but I come from a Scala background, and would be interested in examples in Scala. The best I can find right now is that typeclasses are "open" and ADT's are "closed". It would also be helpful to compare and contrast typeclasses with structural types.
The difference between a type class and an ADT is:
For example, consider the
print
function:Types are static and cannot change throughout the lifetime of a program, therefore when you use a type class the method you use is chosen statically at compile time based on the inferred type at the call site. So in this example I know that I am using the
Char
instance forShow
without even running the program:ADTs let you change a function's behavior dynamically. For example, I could define:
Now, if I call
print2
in some context:... I can't know which branch the
print2
takes unless I know the runtime value ofe
. If thee
is aLeft
then I take theLeft
branch and ife
is aRight
then I take theRight
branch. Sometimes I can statically reason about which constructore
will be, but sometimes I cannot, such as in the following example:Your question actually touches on three distinct concepts: typeclasses, abstract data types and algebraic data types. Confusingly enough, both "abstract" and "algebraic" data types can be abbreviated as "ADT"; in a Haskell context, ADT almost always means "algebraic".
So let's define all three terms.
An algebraic data type (ADT), is a type that can be made by combining simpler types. The core idea here is a "constructor", which is a symbol that defines a value. Think of this like a value in a Java-style enum, except it can also take arguments. The simplest algebraic data type has just one constructor with no arguments:
there is only one¹ value of this type:
Bar
. By itself, this is not very interesting; we need some way to build up bigger types.The first way is to give our constructor arguments. For example, we can have our
Bar
s take an int and a string:Now
Foo
has many different possible values:Bar 0 "baz"
,Bar 100 "abc"
and so on. A more realistic example might be a record for an employee, looking something like this:The other way to build up more complicated types is by having multiple constructors to choose from. For example, we can have both a
Bar
and aBaz
:Now values of type
Foo
can be eitherBar
orBaz
. This is in fact exactly how booleans work;Bool
is defined as follows:It works exactly how you'd expect. Really interesting types can use both methods to combine themselves. As a rather contrived example, imagine shapes:
A shape can either be a rectangle, defined by its two corners, or a circle which is a center and a radius. (We'll just define
Point
as(Int, Int)
.) Fair enough. But here, we run into a snag: it turns out that other shapes also exist! If some heretic who believes in triangles wants to use our type in their model, can they add aTriangle
constructor after the fact? Unfortunately not: in Haskell, algebraic data types are closed, which means you cannot add new alternatives after the fact.One important thing you can do with an algebraic data type is pattern match on it. This basically means being able to branch on the alternatives of an ADT. As a very simple example, instead of using an if expression, you could pattern match on
Bool
:If your constructors have arguments, you can also access those values by pattern matching. Using
Shape
from above, we can write a simplearea
function:The
_
just means we don't care about the value of a point's center.This is just a basic overview of algebraic data types: it turns out there's quite a bit more fun to be had. You might want to take a look at the relevant chapter in Learn You a Haskell (LYAH for short) for more reading.
Now, what about abstract data types? This refers to a different concept. An abstract data type is one where the implementation is not exposed: you don't know what the values of the type actually look like. The only thing you can do with it is apply functions exported from its module. You can't pattern match on it or construct new values yourself. A good example in practice is
Map
(fromData.Map
). The map is actually a particular kind of binary search tree, but nothing in the module lets you work with the tree structure directly. This is important because the tree needs to maintain certain additional invariants which you could easily mess up. So you only ever useMap
as an opaque blob.Algebraic and abstract types are somewhat orthogonal concepts; it's rather unfortunate that their names make it so easy to mistake one for the other.
The final piece of the puzzle is the typeclass. A typeclass, unlike algebraic and abstract data types, is not a type itself. Rather, think of a typeclass as a set of types. In particular, a typeclass is the set of all types that implement certain functions.
The simplest example is
Show
, which is the class of all types that have a string representation; that is, all typesa
for which we have a functionshow ∷ a → String
. If a type has ashow
function, we say it is "inShow
"; otherwise, it isn't. Most types you know likeInt
,Bool
andString
are all inShow
; on the other hand, functions (any type with a→
) are not inShow
. This is why GHCi cannot print a function.A typeclass is defined by which functions a type needs to implement to be part of it. For example,
Show
could be defined² just by theshow
function:Now to add a new type like
Foo
toShow
, we have to write an instance for it. This is the actual implementation of theshow
function:After this,
Foo
is inShow
. We can write an instance forFoo
anywhere. In particular, we can write new instances after the class has been defined, even in other modules. This is what it means for typeclasses to be open; unlike algebraic data types, we can add new things to typeclasses after the fact.There is more to typeclasses too; you can read about them in the same LYAH chapter.
¹ Technically, there is another value called ⊥ (bottom) as well, but we'll ignore it for now. You can learn about ⊥ later.
² In reality,
Show
actually has another possible function that takes a list ofa
s to aString
. This is basically a hack to make strings look pretty since a string is just a list ofChar
s rather than its own type.ADTs (which in this context are not Abstract Data Types, which is even another concept, but Algebraic Data Types) and type classes are completely different concepts which solve different problems.
ADT, as follows from the acronym, is a data type. ADTs are needed to structure your data. The closest match in Scala, I think, is a combination of case classes and sealed traits. This is the primary mean of constructing complex data structures in Haskell. I think the most famous example of ADT is
Maybe
type:This type has a direct equivalent in standard Scala library, called
Option
:This is not exactly how
Option
is defined in the standard library, but you get the point.Basically ADT is a combination (in some sense) of several named tuples (0-ary, as
Nothing
/None
; 1-ary, asJust a
/Some(value)
; higher arities are possible too).Consider the following data type:
This is simple binary tree. Both of these definitions read basically as follows: "A binary tree is either a
Leaf
or aBranch
; if it is a branch, then it contains some value and two other trees". What this means is that if you have a variable of typeTree
then it can contain either aLeaf
or aBranch
, and you can check which one is there and extract contained data, if needed. The primary mean for such checks and extraction is pattern matching:This concept is very simple but also very powerful.
As you have noticed, ADTs are closed, i.e. you cannot add more named tuples after the type has been defined. In Haskell this is enforced syntactically, and in Scala this is achieved via
sealed
keyword, which disallows subclasses in other files.These types are called algebraic for a reason. Named tuples can be considered as products (in mathematical sense) and 'combinations' of these tuples as a summation (also in mathematical sense), and such consideration has deep theoretical meaning. For example, aforementioned binary tree type can be written like this:
But I think this is out of scope for this question. I can search for some links if you want to know more.
Type classes, on the other hand, are a way to define polymorphic behavior. Roughly type classes are contracts which certain type provides. For example, you know that your value
x
satisfies a contract which defines some action. Then you can call that method, and actual implementation of that contract is then chosen automatically.Usually type classes are compared with Java interfaces, for example:
Using this comparison, instances of type classes match with implementation of interfaces:
There are very important differences between interfaces and type classes. First, you can write custom type class and make any type an instance of it:
But you cannot do such thing with interfaces, that is, you cannot make an existing class implement your interface. This feature, as you also have noticed, means that type classes are open.
This ability to add type class instance for existing types is a way to solve expression problem. Java language does not have means for solving it, but Haskell, Scala or Clojure have.
Another difference between type classes and interfaces is that interfaces are polymorphic only on first argument, namely, on implicit
this
. Type classes are not restricted in this sense. You can define type classes which dispatch even on return value:It is impossible to do this with interfaces.
Type classes can be emulated in Scala using implicit parameters. This pattern is so useful that in recent Scala versions there is even a special syntax which simplifies its usage. Here is how it is done:
Showable[T]
trait corresponds to type class, and implicit objects definitions correspond to its instances.As you can see, type classes are a kind of interface, but more powerful. You can even select different implementations of type classes, while the code which uses them remains the same. This power, however, comes at the cost of boilerplate and extra entities.
Note that it is possible to write Haskell equivalent of above Scala program, but it will require writing multiple modules or
newtype
wrappers, so I'm not presenting it here.BTW, Clojure, a Lisp dialect working on JVM, has protocols, which combine interfaces and type classes. Protocols are dispatched on single first argument, but you can implement a protocol for any existing type.