Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: difference between a typeclass and an ADT?

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.

like image 707
James McCabe Avatar asked Sep 29 '13 18:09

James McCabe


People also ask

What is Scala ADT?

ADTs are commonly used in Scala. Simply put, an algebraic data type is any data that uses the Product or Sum pattern. The question now shifts to understanding what Product and Sum Types are.

What is the difference between an ADT and a class in C ++?

What is the difference between an ADT and a class in C++? In an ADT, the user does not have access to the implementation details. applies to the entire file. If you have a class defined in separate files, and change the way a class is defined, which files need to be re-compiled?

Does Haskell have classes?

Haskell classes are roughly similar to a Java interface. Like an interface declaration, a Haskell class declaration defines a protocol for using an object rather than defining an object itself.


1 Answers

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:

data Maybe a = Nothing | Just a

This type has a direct equivalent in standard Scala library, called Option:

sealed trait Option[+T]
case class Some[T](value: T) extends Option[T]
case object None extends Option[Nothing]

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, as Just a/Some(value); higher arities are possible too).

Consider the following data type:

-- Haskell
data Tree a = Leaf | Branch a (Tree a) (Tree a)
// Scala
sealed trait Tree[+T]
case object Leaf extends Tree[Nothing]
case class Branch[T](value: T, left: Tree[T], right: Tree[T]) extends Tree[T]

This is simple binary tree. Both of these definitions read basically as follows: "A binary tree is either a Leaf or a Branch; 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 type Tree then it can contain either a Leaf or a Branch, 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:

-- Haskell
showTree :: (Show a) => Tree a -> String
showTree tree = case tree of
  Leaf                    -> "a leaf"
  Branch value left right -> "a branch with value " ++ show value ++ 
                             ", left subtree (" ++ showTree left ++ ")" ++
                             ", right subtree (" ++ showTree right ++ ")"
// Scala
def showTree[T](tree: Tree[T]) = tree match {
  case Leaf => "a leaf"
  case Branch(value, left, right) => s"a branch with value $value, " +
                                     s"left subtree (${showTree(left)}), " +
                                     s"right subtree (${showTree(right)})"
}

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:

Tree a = 1 + a * (Tree a) * (Tree a)

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:

-- Haskell
class Show a where
    show :: a -> String
// Java
public interface Show {
    String show();
}
// Scala
trait Show {
  def show: String
}

Using this comparison, instances of type classes match with implementation of interfaces:

-- Haskell
data AB = A | B

instance Show AB where
  show A = "A"
  show B = "B"
// Scala
sealed trait AB extends Show
case object A extends AB {
  val show = "A"
}
case object B extends AB {
  val show = "B"
}

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:

class MyShow a where
  myShow :: a -> String

instance MyShow Int where 
  myShow x = ...

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:

class Read a where
  read :: String -> a

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:

trait Showable[T] {
  def show(value: T): String
}

object ImplicitsDecimal {
  implicit object IntShowable extends Showable[Int] {
    def show(value: Int) = Integer.toString(value)
  }
}

object ImplicitsHexadecimal {
  implicit object IntShowable extends Showable[Int] {
    def show(value: Int) = Integer.toString(value, 16)
  }
}

def showValue[T: Showable](value: T) = implicitly[Showable[T]].show(value)
// Or, equivalently:
// def showValue[T](value: T)(implicit showable: Showable[T]) = showable.show(value)

// Usage
{
  import ImplicitsDecimal._
  println(showValue(10))  // Prints "10"
}
{
  import ImplicitsHexadecimal._
  println(showValue(10))  // Prints "a"
}

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.

like image 54
Vladimir Matveev Avatar answered Sep 30 '22 13:09

Vladimir Matveev