Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why prefer implicit val over implicit object

When asking questions about implicits a common suggestion / recommendation / advice that is given together with the answer (or sometimes that is the answer itself) is to use implicit vals with excplicit type signatures instead of using implicit objects.

But, what is the reason behind that?

like image 386
Luis Miguel Mejía Suárez Avatar asked Mar 02 '23 20:03

Luis Miguel Mejía Suárez


1 Answers

"TL;DR;" The reason is that an implici val with an explicit type signature has the exact type you want whereas an implicit object has a different type.

The best way to show why that could be a problem is with a simple example:

// Having the following definitions.

trait SomeTrait[T] {
  def f: T
}

trait ExtendedTrait[T] extends SomeTrait[T] {
  def g: T
}

implicit val SomeStringVal: SomeTrait[String] = new SomeTrait[String] {
  override def f: String = "from val f"
}

implicit val ExtendedStringVal: ExtendedTrait[String] = new ExtendedTrait[String] {
  override def f: String = "from extended val f"
  override def g: String = "from extended val g"
}

implicit object ExtendedStringObject extends ExtendedTrait[String] {
  override def f: String = "from extended obj f"
  override def g: String = "from extended obj g"
}

// What will be the result of:
implicitly[SomeTrait[String]].f

Remember that:

If there are several eligible arguments which match the implicit parameter's type, the most specific one will be chosen using the rules of static overloading resolution.

Well then, the answer is: "from extended obj f".
The above (somewhat surprising) result, is caused because objects have their own type (singleton type ExtendedStringObject.type which is a subtype of ExtendedTrait[String]), as such, it is more "specific" than the others.

Now, the reason why that could be a problem is that most people are not aware that the object has its own type and that it is more specific than what they believe. Nevertheless, this could also make the object to be picked when you would not want it to be; for example:

// If instead we only had this:

implicit val ExtendedStringVal: ExtendedTrait[String] = new ExtendedTrait[String] {
  override def f: String = "from extended val f"
  override def g: String = "from extended val g"
}

implicit object ExtendedStringObject extends SomeTrait[String] {
  override def f: String = "from extended obj f"
}

// Then what will be the result of:
implicitly[SomeTrait[String]].f

In this case, we will have an "ambiguous implicit values" exception; because both options are equally specific.


Is the rules universal?

No.

As with most things in software engineering, nothing is universal.
There are cases where using an implicit val with an explicit type signature is either not possible (for example, because the type is not writable in source code even if the compiler knows it exists) or it would not produce the correct result whereas an implicit object would do.
A simple example would be:

trait A {
  type X
}

object A {
  type Aux[XX] = A { type X = XX }
}

class B extends A {
  type X = T

  class T
}

implicit object b extends B
implicit object b1 extends B

Such that you can ask for implicitly[A.Aux[b.T]] whereas using implicit val b: B = new B {} would have not worked.
(code running here full context here).

However, it could be said that those are rare (advanced) cases. As such, this could be considered a good guideline.

like image 79
3 revs Avatar answered Mar 10 '23 22:03

3 revs