1

I have a classic Encoder typeclass.

trait Encoder[A] {
  def encode(a: A): String
}

I have two questions

Question 1 : where does the divergence comes from :

[error] … diverging implicit expansion for type sbopt.Test.Encoder[None.type]
[error] starting with value stringEncoder in object Test
[error]   show(None)
implicit val stringEncoder = new Encoder[String] {
  override def encode(a: String): String = a
}
implicit def optionEncoder[A: Encoder]: Encoder[Option[A]] =
  (a: Option[A]) => {
    val encoderA = implicitly[Encoder[A]]
    a.fold("")(encoderA.encode)
  }
implicit def someEncoder[A: Encoder]: Encoder[Some[A]] =
  (a: Some[A]) => {
    val encoderA = implicitly[Encoder[A]]
    encoderA.encode(a.get)
  }
implicit def noneEncoder[A: Encoder]: Encoder[None.type] =
  (_: None.type) => ""

def show[A: Encoder](a: A) = println(implicitly[Encoder[A]].encode(a))

show(None)

Question 2 : I saw in circe that the Encoder is not contravariant. What are the pros and cons ?

trait Encoder[-A] {
  def encode(a: A): String
}

implicit val stringEncoder: Encoder[String] = (a: String) => a

implicit def optionEncoder[A: Encoder]: Encoder[Option[A]] =
  (a: Option[A]) => {
    val encoderA = implicitly[Encoder[A]]
    a.fold("")(encoderA.encode)
  }

def show[A: Encoder](a: A) = println(implicitly[Encoder[A]].encode(a))

show(Option("value"))
show(Some("value"))
show(None)
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Yann Moisan
  • 8,161
  • 8
  • 47
  • 91
  • There is no need for a **None** and **Some** encoders. The **Option** one should be enough. – Luis Miguel Mejía Suárez Sep 23 '20 at 12:26
  • @LuisMiguelMejíaSuárez It depends on whether you want to specify type parameter of `show` explicitly (or provide type-ascription hint or use smart constructors `.some`, `.none[A]`) and on how much type-level you are going to be further on. – Dmytro Mitin Sep 23 '20 at 12:36

1 Answers1

1

Regarding 1.

Your definition of noneEncoder is not good. You have an extra context bound (and even extra type parameter).

With

implicit def noneEncoder/*[A: Encoder]*/: Encoder[None.type] =
  (_: None.type) => ""

it compiles:

show[Option[String]](None)
show[None.type](None)
show(None)

Your original definition of noneEncoder meant that you had an instance of Encoder for None.type provided you had an instance for some A (not constrained i.e. to be inferred). Normally this works if you have the only implicit (or at least the only higher-priority implicit). For example if there were only stringEncoder and original noneEncoder then show[None.type](None) and show(None) would compile.

Regarding 2.

PROS. With contravariant Encoder

trait Encoder[-A] {
  def encode(a: A): String
}

you can remove someEncoder and noneEncoder, optionEncoder will be enough

show(Some("a"))
show[Option[String]](Some("a"))
show[Option[String]](None)
show[None.type](None)
show(None)

CONS. Some people believe that contravariant type classes behave counterintuitively:

https://github.com/scala/bug/issues/2509

https://groups.google.com/g/scala-language/c/ZE83TvSWpT4/m/YiwJJLZRmlcJ

Maybe also relevant: In scala 2.13, how to use implicitly[value singleton type]?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • With contravariance, I have a compilation error if I have a implicit def listEncoder[A: Encoder]: Encoder[List[A]] = ??? in scope. Is it the same issues as the one you mentionned ? – Yann Moisan Sep 23 '20 at 13:00
  • 1
    Contravariance is rarely a good idea with implicit/typeclass: https://groups.google.com/g/scala-language/c/ZE83TvSWpT4/m/YiwJJLZRmlcJ – cchantep Sep 23 '20 at 13:15
  • 1
    @YannMoisan No. If you have actual `diverging implicit expansion` (not the one with forgotten `A`) `Lazy` (or sometimes [by-name implicits](https://github.com/scala/scala/releases/tag/v2.13.0)) can fight against that https://gist.github.com/DmytroMitin/9983235f1e8af08740e44c4e64e9bd82 – Dmytro Mitin Sep 23 '20 at 13:31
  • @DmytroMitin That is exactly the issue. I don't know why it doesn't happen with invariant `Encoder`. If I understand correctly, I need either to add a deps to shapeless or target only Scala 2.13 ? (I find it hard to encounter this kind of hard problems when doing simple things). – Yann Moisan Sep 23 '20 at 14:02
  • @cchantep Thanks for the link. Sometimes type classes have to be co/contra/invariant https://stackoverflow.com/questions/54795247/how-can-scala-traits-be-stripped-automatically-during-implicit-search/ https://stackoverflow.com/questions/52653691/defining-instances-of-a-third-party-typeclass-implicit-not-found-but-explicit-w/ https://stackoverflow.com/questions/47079971/how-convert-from-trait-with-type-member-to-case-class-with-type-parameter-an-vic `Unpack1[-PP, FF[_], A]` https://github.com/milessabin/shapeless/blob/master/project/Boilerplate.scala#L544 – Dmytro Mitin Sep 23 '20 at 14:10
  • @cchantep `ToHList[-Repr, L <: Nat]` https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/ops/sizeds.scala#L26 `FnToProduct[-F]` https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/ops/functions.scala#L27 – Dmytro Mitin Sep 23 '20 at 14:11
  • @YannMoisan Well, we are not supposed to understand a specific reason of "diverging implicit expansion" in every particular case. Briefly, compiler uses heuristics whether implicit resolution diverges (first implicit requires second, second requies third, third requires fourth and so on till infinity), sometimes this heuristics reports false positive. "diverging implicit expansion" is a standard situation you should be ready to handle. See https://docs.scala-lang.org/sips/byname-implicits.html Actually, in current case I didn't manage to fix it with by-name implicits, only with `Lazy`. – Dmytro Mitin Sep 23 '20 at 14:23