Help me with implicits

Hi, can someone explain to me why i am getting this strange error?
They seem like two correctly defined implicits which is not in conflict with each other. One is an implicit for type Semigroup[Int] another is an implicit for type Semigroup[(A,B)]

object Main extends App {

  trait Semigroup[S] {
    def append(s1: S, s2: S): S
  }
  implicit val intInstance = new Semigroup[Int] {
    def append(s1: Int, s2: Int) = s1 + s2
  }

  implicit def tupleInstance[A: Semigroup, B: Semigroup]() =
    new Semigroup[(A, B)] {
      def append(t1: (A, B), t2: (A, B)): (A, B) = {
        (
          implicitly[Semigroup[A]].append(t1._1, t2._1),
          implicitly[Semigroup[B]].append(t1._2, t2._2)
        )
      }
    }
}

Error

[error] /Users/laiboonhui/learn/cats/hello/src/main/scala/Hello.scala:8:30: not found: type $anon
[error] implicit val intInstance = new Semigroup[Int] {
[error] ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

I put it on Ammonite and it worked perfectly with scala 2.13.3:

  trait Semigroup[S] {
    def append(s1: S, s2: S): S
  }
  implicit val intInstance = new Semigroup[Int] {
    def append(s1: Int, s2: Int) = s1 + s2
  }

  implicit def tupleInstance[A: Semigroup, B: Semigroup]() =
    new Semigroup[(A, B)] {
      def append(t1: (A, B), t2: (A, B)): (A, B) = {
        (
          implicitly[Semigroup[A]].append(t1._1, t2._1),
          implicitly[Semigroup[B]].append(t1._2, t2._2)
        )
      }
    }

  val x = tupleInstance()

  x.append((2,2), (3,3))

—stdout—

trait Semigroup


val intInstance: Semigroup[Int] = $anon$1@7654bc3a



def tupleInstance[A, B]()(implicit evidence$1: Semigroup[A], evidence$2: Semigroup[B]): Semigroup[(A, B)]









val x: Semigroup[(Int, Int)] = $anon$1@230b4321

val res0: (Int, Int) = (5,5)

You are right. This is the usual idiom. I was just fooling around with the syntax to see what works. Will you be able to explain to me why this idiom is preferred and not the other, since both works

Regarding the original problem it seems a compiler plugin was causing the issue

  compilerPlugin(
    "org.augustjune" %% "context-applied" % "0.1.2"
  )

But by using the “usual idiom” there will be no issues even with the compiler plugin

It looks like the type parameters are checked only if there is an explicit type parameter or it can be derived from the argument from the constructor.
Since none of these two happened, perhaps the compiler looked at those two implicitly commands and found the closest thing to Semigroup[Any]…
But this is all guess work from a person who has no compiler experience :slight_smile:

I think you need to tell the context relationship in your type check. Check my reved constructor:

trait Semigroup[S] {
  def append(s1: S, s2: S): S
}
implicit val intInstance = new Semigroup[Int] {
  def append(s1: Int, s2: Int) = s1 + s2
}

implicit def tupleInstance[A: Semigroup, B: Semigroup]() =
  new Semigroup[(A, B)] {
    def append(t1: (A, B), t2: (A, B)): (A, B) = {
      (
        implicitly[Semigroup[A]].append(t1._1, t2._1),
        implicitly[Semigroup[B]].append(t1._2, t2._2)
      )
    }
  }

val x = tupleInstance()

I think both idioms are OK. I reviewed your code, and it looks good to me :).
The key is to define the types at constructor time. If you don’t define the types, it becomes unpredictable. That is why your constructor is a bit scary IMO. It is too easy to allow anybody to accidentally miss the type parameters.

trait Semigroup[S] {
  def append(s1: S, s2: S): S
}
implicit val intInstance = new Semigroup[Int] {
  def append(s1: Int, s2: Int) = s1 + s2
}

implicit def tupleInstance[A: Semigroup, B: Semigroup]() =
  new Semigroup[(A, B)] {
    def append(t1: (A, B), t2: (A, B)): (A, B) = {
      (
        implicitly[Semigroup[A]].append(t1._1, t2._1),
        implicitly[Semigroup[B]].append(t1._2, t2._2)
      )
    }
  }

val x = tupleInstance[Int, Int]()

Using a context bound (the [A: Semigroup] syntax) is equivalent to adding an implicit parameter (implicit sga: Semigroup[A]). The only difference is, that it does not assign a name to it (at least none that you can use). So the tradeoff between a context bound and an implicit parameter list is mostly the location of verbosity / complexity. With an implicit parameter, you have a longer signature and the information about what a type parameter has to fulfill are further from the type parameter itself. With a context bound on the other hand, you’ll have to use implicitly, because the instance doesn’t have a stable name.

I’d argue there isn’t really a problem with being able to miss the type parameters. You showed, that you used the method this way:

val x = tupleInstance()

But the only reason this works without type parameters at all, is because there is only a single implicit val instance of Semigroup. If you add e.g. another Semigroup for double, you’ll get an error like this:

cmd5.sc:1: ambiguous implicit values:
 both value intInstance in object cmd1 of type => ammonite.$sess.cmd0.Semigroup[Int]
 and value doubleInstance in object cmd4 of type => ammonite.$sess.cmd0.Semigroup[Double]
 match expected type ammonite.$sess.cmd0.Semigroup[A]
val x = tupleInstance()

And even if that works because only one other instance is in scope, you’ll still get type errors when using the methods, because x will have the wrong type.


From looking at the github-page of that plugin, it looks like it is specifically for using context bounds without requiring implicitly. So if it is causing errors when using context bounds, that is probably a bug in that plugin. Maybe it doesn’t work well with using implicitly anyway?

2 Likes

I will just mention that defining implicits without explicit type signatures can lead to “funny” errors.