Type Class
Tags: scala, coding
Published: 2024-04-03 12:26:15

> ***Type class*** is an abstract, parameterized type used to extend the behavior of a closed type without sub-typing it.

## Overview

Suppose, we want to represent an instance of a particular type in its string format. The built-in `toString` is not always useful and is a typical example to understand type class. There are 2 steps to creating a type class

* Implement a type class
    
* Implement type class instances of concrete types.
    

Once the instance is created, there are different approaches to using the instance with slight variations.

## Implement a type class

Let's define a type class to define the method to return a string of the type instance.

```scala
trait Show[T] {
  def show(t: T): String
}
```

A type class is a *trait*. It is parameterized on a type and defines a behavior. The behavior in *Show* is the method *show(t: T)*. This becomes the template to apply to those types that require.

## Implement concrete type class

Let us assume, *Domain* type must be *Showable*. We implement *Show\[Domain\]* and create a type class instance.

```scala
case class Department(name: String)
class ShowDepartment extends Show[Department] {
  override def show(d: Department) = d.name
}
implicit val showDepartmentInstance = new ShowDepartment
```

Type class instance `showDomain` need not be implicit to be a type class instance! This is covered in the usage of the instance. That's it. We have implemented a concrete type class and created a type class instance.

## Usage

Different use cases and convenience required in usage lead to multiple approaches that the type class and its implementation evolve with additional methods. There are two types of using the implementation of concrete type class. They are classified as **instance type** and **syntax type**.

### Instance type

We have an instance of type class created and it could be explicitly used by direct reference. This makes the call site tightly coupled to the type which is probably never a requirement.

```scala
def show(a: Domain): String = showDepartmentInstance.show(a)
```

Let's make the call site polymorphic on the type.

```scala
def show[A](a: A)(implicit ii: Show[A]): String  = ii.show(a)
```

Now the call site is polymorphic on type. The type class instance to be used depends on the type. Hence, the instance is marked implicit. This means a type of `Show[A]` must be present in the implicit scope. This is the reason the type class instance `showDomain` is marked implicit for the call site to work with `Department`.

#### Sugared usage

Call site still uses an implicit reference to type class instance and can it be eliminated? Being so, the call site becomes concise. The conciseness is in the idea that there is no reference to type class instance in the snippet. Call site is completely expressed as the name of type class and the behavior required.

```scala
def show[A: Show](a: A): String = ???
```

The semantic `show[A: Show]` is referred to as the **context-bound** type. So, it is read as *the call site method* `show` is polymorphic on A provided the Show is bound to A.

In a context-bound call site of a type class instance, there is no direct reference to a type class instance. So, we use `implicitly[_]`.

```scala
def show[A: Show](a: A): String = implicitly[Show[A]].show
```

The design of the type class has forced the call site to implicitly resolve the type class instance whether it is sugared or the de-sugared form. This can be improved by a companion for the type class which summons the implicit instance of the type class.

```scala
// companion for the type class `Show`
object Show {
  def apply[T](implicit instance: Show[T]) = instance
}
// call site
def show[A: Show](a: A) = Show[A].show(a)
```

Companion object only summons an implicit instance. It does not create one. The complete implementation looks this way

```scala
// type class
trait Show[A] {
  def show(a: A): String
}
object Show {
  // summons an implicit instance
  def apply[A](a: A)(implicit i: Show[A]) = i
}

case class Department(name: String)
object Department {
  // creation of implicit instance
  implicit val showDepartmentInstance = new Show[Department] {
    override def show(d: Department) = d.name
  }
}

// call site #1 with implicit parameters defined
def show_v1[A](a: A)(implicit ii: Show[A]) = ii.show(a)
// call site #2 with context bound implicit instance
def show_v2[A: Show](a: A) = Show[A].show(a)

// test
val d = Department("Engineering")
show_v1(d) // String: Engineering
show_v2(d) // String: Engineering
```

The problem statement could further be extended but the solution pattern is already simplified. In the current domain model, let's suppose there is a `Student` belonging `Department`.

```scala
case class Student(name: String, department: Department)
object Student {
  // create implicit instance
  implicit val showStudentInstance = new Show[Student] {
    override def show(s: Student) = 
      s"${s.name}, ${Show[Department].show(s.department)}"
  }
}

// test
val s = Student("Martin", Domain("Engineering"))
show_v1(s) // String: "Martin, Engineering"
show_v2(s) // String: "Martin, Engineering"
```

`Student` has an instance of `Department`. So, the `show()` of the Student requires the `show()` of the Department. Since there is one in the implicit scope already, it only needs to be summoned. Hence, `Show[Department]`.

However, we notice a redundancy in the way type class instances are getting created. The core difference lies only in the function by which a given instance of `Department` or `Student` would be transformed to String. The rest of it is all boilerplate.

```scala
  // from Department
  implicit val showDepartment = new Show[Department] {
    override def show(d: Department) = d.name
  }
  // from Student
  implicit val showStudentInstance = new Show[Student] {
    override def show(s: Student) = 
      s"${s.name}, ${Show[Department].show(s.department)}"
  }
```

This can be solved by enhancing the companion object of `Show` with a helper method to create instances given a required function. It is just a helper function to create instances but the instances get created when this helper function is called.

```scala
trait Show[A] {
  def show(a: A): String
}
object Show {
  // summons the instance
  def apply[A](implicit inst: Show[A]) = inst

  // helper: to create type class instance.
  def instance[A](f: A => String) = new Show[A] {
    override def show(a: A): String = f(a)
  }
}
case class Department(name: String)
object Department {
  // instance creation
  implicit val showDepartmentInstance = Show.instance[Department](d => d.name)
}
case class Student(name: String, department: Department)
object Department {
  implicit val showStudentInstance = Show.instance[Student](s => s"${s.name]} from ${Show[Department].show(s.department)}")
}
```

All the boilerplate code to create an instance is now replaced by a function.

It appears to be a lengthy topic about type class with instance type of usage but the definition of type class and its concrete implementation define the type class behavior. The rest of it is the evolution of the companion object `Show` from not existing in the bare-bone implementation of type class to a flavor with helpers to create and summon the type class instance.

### Syntax type

With the *syntax type*, we can improve the type class usage with the possibility of calling the `show()` on the given object as if it were defined on the object itself. This is called the *extension method*. This is achieved by having an implicit class wrapping over the instance of type T.

```scala
implicit class ShowOps[A](a: A) {
  def show(implicit instance: Show[A]): String = instance.show(a)
}

case class Department(name: String)
val enggDep = Department("Engineering")
enggDep.show // String: "Engineering"
```

There is no `show()` on `Department` yet, with the syntactic usage of a type class, the method appears to be on the `Deparment`. The method `show()` could have been given any other name as well. It is an arbitrary method name but `show()` makes sense in this case. This is re-written as `(new ShowOps[Department](enggDep)).show(showDomainInstance)` by the compiler. In this implementation of `ShowOps`, it is only the method `show` constraint by the type. An alternate implementation is to enforce the constraint at the class level.

```scala
implicit class ShowOps[A: Show](a: A) {
  def show(): String = Show[A].show(a) 
}
case class Department(name: String)
val enggDep = Department("Engineering")
enggDep.show
```

I prefer enforcing the constraint at the lowest unit of work which, in this case, is the method`show` leaving the class generic.

#### Where to place Ops implementation?

Class `ShowOps` is generic and can be placed in the companion of `Show`.

```scala
trait Show[A]{
  def show(a: A): String
}
object Show {
  // instantiator
  def instance[A](f: A => String) = new Show[A] {
    def show(a: A): String = f(a)
  }
  // summoner
  def apply[A](implicit instance: Show[A]): Show[A] = instance

  object ops {
    implicit class ShowOps[A](a: A) {
      def show(implicit instance: Show[A]) = instance.show(a)
    }
  }
}

import Show._
import Show.ops._
case Department(name: String)
val d = Department("Finance")
d.show // String: Finance
```

Type class is an interesting concept and I have been reading about it from different sources. So, I decided to pen down my learning from different sources like [scala-lang.org](https://docs.scala-lang.org), [scalac.io](https://scalac.io), with examples that I could relate to better.