2

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 trough it easy when i stumble upon something that i do not quite understand and was hopping that someone could help clarify it.

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.

I don't get this last comment?

Dmytro Mitin
  • 34,874
  • 2
  • 15
  • 47
MaatDeamon
  • 7,821
  • 5
  • 46
  • 96

2 Answers2

3

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)
slouc
  • 8,715
  • 3
  • 13
  • 36
  • The first difference that i see is `import show.Show import show.Show.ops._` while you only put `import show.Show.ops._` . So it means the author did a mistake there right ? Because that's what was confusing me. With the Show object and the show.ops object you get the instance and the method that resolve them, while your solution only import the method that resolve the implicit `show.Show.ops._`, and in this case, yes the first place to look at is your current scope. AM I correct ? – MaatDeamon Sep 21 '20 at 16:06
  • `import show.Show` is for the type `Show` (the trait). I omitted it simply because I had it in the same file already. However, `import show.Show._` (underscore!) is what imports the values from `Show` companion object, and this is what I did in my last example. That one prevents users of the type class from defining their own implicit instances. If we don't import `Show._`, they get resolved as category 2 values because they exist in the `Show` object. This is a Scala feature - when you need implicit F[A] for some A, compiler will look into A's companion object (among other places). – slouc Sep 21 '20 at 17:34
  • I will add `import Show` to my examples to avoid confusion. – slouc Sep 21 '20 at 17:36
1

The author of this tutorial calls higher-priority implicits (from local scope) Category-1 implicits and lower-priority implicits (from implicit scope) Category-2 implicits.

Where does Scala look for implicits?

https://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html

Dmytro Mitin
  • 34,874
  • 2
  • 15
  • 47