Why Type Classes?

The concept of Type class is one solution for the expression problem which is a major challenge for programing languages even today. Lets explore what this problem means.

The Expression Problem

According to Wikipedia,

The goal is to define a datatype by cases, where one can 
add new cases to the datatype and new functions over the 
datatype, without recompiling existing code, and while 
retaining static type safety (e.g., no casts).

Lets boil this down.

We can consider computer programs as a combination of data and operations. If we want to add new data or operations we should not touch the existing code. This is the challenge expressed by the Expression problem.

One of the major distinctions between Functional Programing and Object Oriented Programing is, in functional programing, data and related operations are completely separated and in OOP data encapsulates the related operations.

In Haskell, the basic programing style is to define your ADTs(Algebraic Data Types) at the beginning of the program.

module Data where

data Expression = Constant Int | Plus Expression Expression

evaluate :: Expression -> Int
evaluate (Constant i) = i
evaluate (Plus e1 e2) = result e1 + result e2 

Constant and Plus are data constructors for type Expression


interface Expression {
    int evaluate;
}

class Constant extends Expression {
    private int value; 
    
    // ..
    
    public int evaluate() {
        return this.value;
    }
}

class Plus extends Expression {
    private Expression left, right;
    
    // ...
    
    public int evaluate() {
        return left.evaluate() + right.evaluate();
    } 
} 

Constant and Plus are separate implementations of interface Expression

Lets assume that a pretty printer has to be added for type Expression.


module PrettyPrinter where

import Data

stringify :: Expression -> String
stringify (Constant i) = show i
stringify (Plus l r) = stringify l ++ " + " ++ stringify r

It seems it is easy to add a new operation like stringify. Let’s try the same in Java.


interface Expression {
    int evaluate;
    String stringify;
}

class Constant extends Expression {
    private int value; 
    
    // ..
    
    public int evaluate() {
        return this.value;
    }
    
    public String stringify() {
        return this.value.toString();
    }
}

class Plus extends Expression {
    private Expression left, right;
    
    // ...
    
    public int evaluate() {
        return left.evaluate() + right.evaluate();
    } 
    
    public String stringify() {
        return left.stringify() + " + " + right.stringify();
    }
} 

It is not easy to add a new operation like stringify, because all the implementations of Expression has to be updated with the new operation.

Lets try to add a new data type for Expression

    
module Data where

data Expression = Constant Int 
                | Plus Expression Expression 
                | Minus Expression Expression 

evaluate :: Expression -> Int
evaluate (Constant i) = i
evaluate (Plus e1 e2) = evaluate e1 + evaluate e2 
evaluate (Minus e1 e1) = evaluate e1 - evaluate e2

module PrettyPrinter where

import Data

stringify :: Expression -> String
stringify (Constant i) = show i
stringify (Plus l r) = stringify l 
                        ++ " + " ++ stringify r
stringify (Minus e1 e1) = stringify l 
                           ++ " - " ++ stringify r

In Haskell, its not easy to add a new data type since all the functions has to be updated with the new value.

    
public class Minus extends Expression {
    private Expression left;
    private Expression right;
    
    @Override
    public String stringify() {
        return left.stringify() + " - " + right.stringify();
    }
}

In Java, a new type can be added without touching the existing code.

Therefore, in a functional programing language like haskell, its easy to get data extensibility but not operator extensibility. In an OOP language like Java, its easy to get data extensibility but not operator extensibility.

Can we use Visitor Pattern?

Visitor Patterns does not exactly solve the problem but it’s worth checking out.


interface Visitor<R> {
    R visit(Constant that);
    R visit(Plus that);
}

public abstract class Expression {
    public abstract <R> R accept(Visitor<R> v);
}

public class Stringify implements Visitor<String> {
    public String visit(Constant that) {
        return that.info.toString;
    }
    
    public String visit(Plus that) {
        return that.left.accept(this) + 
                " + " + that.right.accept(this);
    }
}

public class Constant extends Expression {
    private int info;
    
    @Override
    public <R> R accept(Visitor<R> v) {
        return v.visit(this);
    } 
}

public class Plus extends Expression {
    private Expression left, right;
    
    @Override
    public <R> R accept(Visitor<R> v) {
        return v.visit(this);
    }
}

The Visitor contains a trace of all the data variants. Because there’s a visit implementation for each of the data variants. If a new data variant is added, then the Visitor interface has to be changed and new implementation for that variant should be added. Therefore this is not a neat solution.

Type Classes to the Rescue

Lets breakdown the data declarations.


data Constant = Constant Int
data Plus l r = Plus l r  

Define a generic type class Expression and add instances for all data types.


-- type class
class Expression x

instance Expression Constant

instance (Expression l, Expression r) => Expression (Plus l r)
    

Evaluation functionality is defined in a separate type class.


-- type class that contains evaluate operation
class Expression x => Evaluate x where
  evaluate :: x -> Int

instance Evaluate Constant where
  evaluate (Constant i) = i
  
instance (Expression l, Expression r) => 
                           Evaluate (Plus l r) where
  evaluate (Plus l r) = evaluate l + evaluate r
      

Lets try to extend data by adding Minus


data Minus l r = Minus l r

instance (Expression l, Expression r) => Expression (Minus l r)

instance (Expression l, Expression r) => Evaluate (Minus l r) where
  evaluate (Minus l r) = evaluate l - evaluate r
      

Lets try to extend operations by adding stringify

class Expression x => PrettyPrint x where
  stringify :: x -> string
  
instance PrettyPrint Constant where
  stringify (Constant i) = show i
  
instance (Expression l, Expression r) => 
              PrettyPrint (Expression (Plus l r)) where
  stringify (Plus l r) = stringify l 
                          ++ " + " ++ stringify r

instance (Expression l, Expression r) => 
              PrettyPrint (Expression (Minus l r)) where
  stringify (Minus l r) = stringify l 
                          ++ " - " ++ stringify r

That’s it. We have extended both data and operators without touching old code.

Ideas and comments are welcome.

References: Dr Ralf Lmmel’s Presentation

Written on February 6, 2018