6
votes

I'm reading through and working my way through using type classes and I came across this way of defining type classes from the Shapeless guide:

So here goes the example:

object CsvEncoder {
  // "Summoner" method
  def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] =
    enc
  // "Constructor" method
  def instance[A](func: A => List[String]): CsvEncoder[A] =
    new CsvEncoder[A] {
      def encode(value: A): List[String] =
        func(value)
      }
    // Globally visible type class instances
}

What I do not understand is the need for the apply method? What is it doing in this context above?

Later on, the guide describes how I could create a type class instance:

implicit val booleanEncoder: CsvEncoder[Boolean] =
  new CsvEncoder[Boolean] {
    def encode(b: Boolean): List[String] =
      if(b) List("yes") else List("no") 
  } 

is actually shortened to:

implicit val booleanEncoder: CsvEncoder[Boolean] =
instance(b => if(b) List("yes") else List("no"))

So my question now is, how does this work? What I do not get is the need for the apply method?

EDIT: I came across a blog post that describes the steps in creating type classes as below:

  1. Define typeclass contract trait Foo.
  2. Define a companion object Foo with a helper method apply that acts like implicitly, and a way of defining Foo instances typically from a function.
  3. Define FooOps class that defines unary or binary operators.
  4. Define FooSyntax trait that implicitly provides FooOps from a Foo instance.

So what is the deal with point number 2, 3 and 4?

3

3 Answers

5
votes

Another thing to note is that in shapeless the apply method is not only for cuter syntax.

Take for instance this simplified version of shapeless' Generic and some case class Foo.

trait Generic[T] {
  type Repr
}
object Generic {
  def apply[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen

  /* lots of macros to generate implicit instances omitted */
}

case class Foo(a: Int, b: String)

Now when I call Generic[Foo] I will get an instance that is typed as Generic[Foo] { type Repr = Int :: String :: HNil }. But if I call implicitly[Generic[Foo]] all the compiler knows about the result is that it's a Generic[Foo]. In other words: the concrete type of Repr is lost and I can't do anything useful with it. The reason is that implicitly is implemented as follows:

def implicitly[T](implicit e: T): T = e

That method declaration basically says: if you ask for a T I promise to give you a T, if I find one, and nothing more. So that means you'd have to ask implicitly[Generic[Foo] { type Repr = Int :: String :: HNil }] and that defeats the purpose of having automatic derivation.

5
votes

Most of those practices came from Haskell (basically an intention to mimic Haskell's type-classes is a reason for so much boilerplate), some of it is just for convenience. So,

2) As @Alexey Romanov mentioned, companion object with apply is just for convenience, so instead of implicitly[CsvEncoder[IceCream]] you could write just CsvEncoder[IceCream] (aka CsvEncoder.apply[IceCream]()), which will return you a required type-class instance.

3) FooOps provides convenience methods for DSLs. For instance you could have something like:

trait Semigroup[A] {
   ...
   def append(a: A, b: A)
}

import implicits._ //you should import actual instances for `Semigroup[Int]` etc.
implicitly[Semigroup[Int]].append(2,2)

But sometimes it's inconvenient to call append(2,2) method, so it's a good practice to provide a symbolic alias:

  trait Ops[A] {
    def typeClassInstance: Semigroup[A]
    def self: A
    def |+|(y: A): A = typeClassInstance.append(self, y)
  }

  trait ToSemigroupOps {
    implicit def toSemigroupOps[A](target: A)(implicit tc: Semigroup[A]): Ops[A] = new Ops[A] {
      val self = target
      val typeClassInstance = tc
    }
  }

  object SemiSyntax extends ToSemigroupOps

4) You can use it as follows:

import SemiSyntax._ 
import implicits._ //you should also import actual instances for `Semigroup[Int]` etc.

2 |+| 2

If you wonder why so much boilerplate, and why scala's implicit class syntax doesn't provide this functionality from scratch - the answer is that implicit class actually provides a way to create DSL's - it's just less powerful - it's (subjectively) harder to provide operation aliases, deal with more complex dispatching (when required) etc.

However, there is a macro solution that generates boilerplate automatically for you: https://github.com/mpilquist/simulacrum.


One another important point about your CsvEncoder example is that instance is convenience method for creating type-class instances, but apply is a shortcut for "summoning" (requiring) those instances. So, first one is for library extender (a way to implement interface), another one is for a user (a way to call a particular operation provided for that interface).

2
votes

Quoting the guide immediately after the object CsvEncoder definition:

The apply method ... allows us to summon a type class instance given a target type:

CsvEncoder[IceCream]
// res9: CsvEncoder[IceCream] = ...