I suspect most are aware of the Show example to introduce type class.
I found out this blog post https://scalac.io/typeclasses-in-scala/, and was going through it easy when I stumble upon something that I do not quite understand.
I understand everything in the blog post expect when it talks about implicit categories:
From the type class full definition with syntax and object interface
trait Show[A] {
def show(a: A): String
}
object Show {
def apply[A](implicit sh: Show[A]): Show[A] = sh
//needed only if we want to support notation: show(...)
def show[A: Show](a: A) = Show[A].show(a)
implicit class ShowOps[A: Show](a: A) {
def show = Show[A].show(a)
}
//type class instances
implicit val intCanShow: Show[Int] =
int => s"int $int"
implicit val stringCanShow: Show[String] =
str => s"string $str"
}
We get the following comment:
We may encounter a need to redefine some default type class instances. With the implementation above, if all default instances were imported into scope we cannot achieve that. The compiler will have ambiguous implicits in scope and will report an error.
We may decide to move the show function and the ShowOps implicit class to another object (let say ops) to allow users of this type class to redefine the default instance behaviour (with Category 1 implicits, more on categories of implicits). After such a modification, the Show object looks like this:
object Show {
def apply[A](implicit sh: Show[A]): Show[A] = sh
object ops {
def show[A: Show](a: A) = Show[A].show(a)
implicit class ShowOps[A: Show](a: A) {
def show = Show[A].show(a)
}
}
implicit val intCanShow: Show[Int] =
int => s"int $int"
implicit val stringCanShow: Show[String] =
str => s"string $str"
}
Usage does not change, but now the user of this type class may import only:
import show.Show
import show.Show.ops._
Default implicit instances are not brought as Category 1 implicits (although they are available as Category 2 implicits), so it’s possible to define our own implicit instance where we use such type class.
What is meant by this last comment?
Implicit instances for Show[Int]
and Show[String]
are defined in the Show
companion object, so whenever a value of type Show
is used, type class instances will be available. However, they can be overridden by the user. This makes them category 2 implicits - they come from the implicit scope.
Implicits that are brought into scope by direct import, on the other hand, are category 1 implicits. They come from the local scope and they cannot be overridden. Directly importing the implicits is hence the same as defining them on the spot - both are considered category 1. If there is more than one category 1 implicit value of the same type present in local scope, compiler will complain.
What the article says is, put your implicit implementations in the companion object, but put the "machinery" in the ops
. That way users of your type class can just import the machinery which allows them to do e.g. 42.show
, without bringing in the type class instances as category 1 values.
Our users can then do:
import show.Show
import show.Show.ops._
// available from Show as category 2 implicit:
println(42.show) // "int 42"
as well as:
import show.Show
import show.Show.ops._
// overriding category 2 implicit with our own category 1 implicit:
implicit val myOwnIntCanShow: Show[Int] = int => s"my own $int"
println(42.show) // prints "my own 42"
But if we didn't have the ops
object and we simply put everything in the Show
object, then whenever our users would do import Show._
(and they would need to, in order to be able to do 42.show
) they would receive all our implicits as category 1 values and wouldn't be able to override them:
import show.Show
// Assuming everything is in `Show` (no `ops`)...
import show.Show._
implicit val myOwnIntCanShow: Show[Int] = int => s"my own $int"
// this line doesn't compile because implicits were brought
// into scope as category 1 values (via import Show._)
println(42.show)