Can typeclasses solve the expression problem?

Typeclasses in Haskell roughly correspond to interfaces in object-oriented languages and can be used to solve the Expression Problem. More specifically, since Haskell takes parametric polymorphism quite seriously, typeclasses are more akin to generic interfaces, but also allow ad-hoc polymorphism.

Let's first define an Expr typeclass with const and add as functions. These functions represent types or variants in the Expression Problem.

class Expr t where
  const :: Double -> t
  add :: t -> t -> t

Let's define the Eval operation as a type with eval as a field accessor. This can be easily defined as an instance of Expr.

newtype Eval = Eval { eval :: Double }

instance Expr Eval where
  const n = Eval n
  add x y = Eval $ eval x + eval y

We can define a value e1 to test that eval e1 produces the correct value.

e1 :: Expr t => t
e1 = add (const 1)
         (add (const 2)
              (const 3))

eval e1 returns 6.0 as expected. Next, let's define the View operation as another type.

import Text.Printf (printf)

newtype View = View { view :: String }

instance Expr View where
  const n = View $ show n
  add x y = View $ printf "(%s + %s)" (view x) (view y)

view e1 returns the string "(1.0 + (2.0 + 3.0))". We can now also add the Mult operation by defining another typeclass MultExpr for it, and defining instances of Eval and View.

class MultExpr t where
  mult :: t -> t -> t

instance MultExpr Eval where
  mult x y = Eval $ eval x * eval y

instance MultExpr View where
  mult x y = View $ printf "(%s * %s)" (view x) (view y)

Now to use MultExpr, simply add another constraint to the polymorphic type t.

e2 :: (Expr t, MultExpr t) => t
e2 = mult (const 2)
          (add (const 2)
               (const 3))

This produces 10.0. That's it! We've solved the Expression Problem using typeclasses to represent data types.

Next, let's approach the problem by representing the operations in the Expression Problem as typeclasses. We begin by defining Const and Add.

data Const   = Const Double
data Add x y = Add x y

These types are used to create expressions, so let's define a typeclass for that purpose.

class Expr e

instance Expr Const
instance (Expr x, Expr y) => Expr (Add x y)

Next, we can define Eval as a typeclass with a single eval function, making Const and Add instances of Eval.

class (Expr e) => Eval e where
  eval :: e -> Double

instance Eval Const where
  eval (Const x) = x

instance (Eval x, Eval y) => Eval (Add x y) where
  eval (Add x y) = eval x + eval y

We can similarly define View.

class (Expr e) => View e where
  view :: e -> String

instance View Const where
  view (Const x) = show x

instance (View x, View y) => View (Add x y) where
  view (Add x y) = printf "(%s + %s)" (view x) (view y)

Additionally, we can introduce a new data type Mult and make it an instance of Expr, Eval and View.

data Mult x y = Mult x y

instance (Expr x, Expr y) => Expr (Mult x y)

instance (Eval x, Eval y) => Eval (Mult x y) where
  eval (Mult x y) = eval x * eval y

instance (View x, View y) => View (Mult x y) where
  view (Mult x y) = printf "(%s * %s)" (view x) (view y)

But wait, there's an issue - the types aren't related in any meaningful way. That means we can't define a function that composes Const and Add even though they're instances of Expr.

getAlgebra x
  | x < 10    = Const 2
  | otherwise = Add (Const 4) (Const 5)

The above function won't compile.

* Couldn't match expected type ‘Const’
              with actual type ‘Add Const Const’
* In the expression: (Add (Const 4) (Const 5))

In fact, defining the expression Add (Const 4) (Const 5) itself won't work, even if the constraint Expr t => t is applied.

e1 :: Expr t => t
e1 = Add (Const 4) (Const 5)    -- fails compilation

Revisiting the approach of using typeclasses to represent data types in the Expression Problem allows an easy definition of getAlgebra.

getAlgebra x
  | x < 10    = const 2
  | otherwise = add (const 4) (const 5)

In conclusion, typeclasses can effectively solve the Expression Problem, but only when they are used to represent data types. This ensures the relationships between types are preserved and used correctly.

The code in this post can be found here along with relevant tests.

Tags: Solving the expression problem