Skip to content

Instantly share code, notes, and snippets.

@tjheslin1
Last active November 19, 2017 13:33
Show Gist options
  • Save tjheslin1/0a2fcbd90a73580e9b9b3c872690e558 to your computer and use it in GitHub Desktop.
Save tjheslin1/0a2fcbd90a73580e9b9b3c872690e558 to your computer and use it in GitHub Desktop.

Taken from Essential Scala by Noel Welsh and Dave Gurnell

The type class pattern separates the implementation of functionality from the type the functionality is provided for.

In Scala, a type class is just a trait.

trait ExampleTypeClass[A] {
  def doSomething(in: A): Foo
}

Full example:

trait HtmlWriter[A] {
  def write(in: A): String
}

object PersonWriter extends HtmlWriter[Person] {
  def write(person: Person) = s"<span>${person.name} &lt;${person.email}&gt;</span>"
}

PersonWriter.write(Person("John", "john@example.com"))

To use a type class we:

  • create implementations of that trait, called type class instances; and
  • typically we mark the type class instances as implicit values.

Implicit Scope:

  1. Normal scope where other identifiers are found (including local scope and imports).
  2. Companion objects of types involved in the method call with the implicit parameter.

For example:

sorted[B >: A](implicit ord: math.Ordering[B]): List[A]

The compiler will look in the following places for Ordering instances:

  • the companion object of List;
  • the companion object of Ordering; and
  • the companion object of the type B, which is the type of elements in the list or any superclass.

Implicit Priority:

Local scope takes precedence over instances found in companion objects (resutls in no ambiguity).

Type Enrichment:

Implicit Classes

implicit class ExtraStringMethods(str: String) {
  val vowels = Seq('a', 'e', 'i', 'o', 'u')
  def numberOfVowels =
    str.toList.filter(vowels contains _).length
}

"the quick brown fox".numberOfVowels
// res: Int = 5

When the compiler processes our call to numberOfVowels, it interprets it as a type error because there is no such method in String. Rather than give up, the compiler a empts to fix the error by searching for an implicit class that provides the method and can be constructed from a String. It finds ExtraStringMethods. The compiler then inserts an invisible constructor call, and our code type checks correctly.

The compiler will not look to construct a chain of implicit classes to access the desired method.

Example Equal[A]

trait Equal[A] {
  def equal(v1: A, v2: A): Boolean
}

object Equal {

  // Type Class Interface Pattern
  def apply[A](implicit instance: Equal[A]): Equal[A] = instance
  
  implicit class ToEqual[A](in: A) {
    def ===(other: A)(implicit equal: Equal[A]): Boolean =
      equal.equal(in, other)
  }
}

implicit val caseInsensitiveEquals = new Equal[String] { def equal(s1: String, s2: String) =
    s1.toLowerCase == s2.toLowerCase
}

import Equal._

"foo".===("FOO")

Context Bounds

def pageTemplate[A](body: A)(implicit writer: HtmlWriter[A]): String = {
  val renderedBody = body.toHtml
  s"<html><head>...</head><body>${renderedBody}</body></html>"
}

can be written as:

def pageTemplate[A : HtmlWriter](body: A): String = {
  val renderedBody = body.toHtml
  s"<html><head>...</head><body>${renderedBody}</body></html>"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment