Search code examples
scalaimplicit-conversionimplicitimplicit-typing

What's the point of implicit conversions as parameters?


I'm reading the docs on implicits in Scala, and there is an example of a function with implicit conversion as parameter:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

I understand how it works, but I don't understand what's the point of writing it like that instead of:

def getIndexExplicit[T](seq: Seq[T], value: T) = seq.indexOf(value)

As far as I know, if the conversion from the argument seq to type Seq[T] exists, the compiler would still allow the call to getIndexExplicit?

To illustrate my point, I prepared this simple example:

def equal42[T](a: T)(implicit conv: T => Int) = conv(a) == 42  // implicit parameter version
def equal42Explicit(a: Int) = a == 42                          // just use the type in the signature

implicit def strToInt(str: String): Int = java.lang.Integer.parseInt(str) // define the implicit conversion from String to Int

And indeed, both functions seem to work in the same way:

scala> equal42("42")
res12: Boolean = true

scala> equal42Explicit("42")
res13: Boolean = true

If there is no difference, what's the point of explicitly defining the implicit conversion?

My guess is that in this simple case it makes no difference, but there must be some more complex scenarios where it does. What are those?


Solution

  • In your super-simple example:

    equal42("42")
    equal42Explicit("42")
    

    is equal to

    equal42("42")(strToInt)
    equal42Explicit(strToInt("42"))
    

    which in case of your definition make no difference.

    BUT if it did something else e.g.

    def parseCombined[S, T](s1: S, s2: S)
                           (combine: (S, S) => S)
                           (implicit parse: S => Option[T]): Option[T] =
      parse(combine(s1, s2))
    

    then when you would apply conversion matters:

    implicit def lift[T]: T => Option[T] = t => Option(t)
    implicit val parseInt: String => Option[Int] = s => scala.util.Try(s.toInt).toOption
    implicit def parseToString[T]: T => Option[String] = t => Option(t.toString)
    parseCombined[Option[Int], String]("1", "2") { (a, b) => a.zip(b).map { case (x, y) => x + y } } // Some("Some(3)")
    parseCombined[String, Int]("1", "2") { _ + _ } //  Some(12)
    

    In the first case arguments were converted before passing (and then again inside), while in the other case they were converted only inside a function at a specific place.

    While this case is somewhat stretched, it shows that having control over when conversion is made might matter to the final result.

    That being said such usage of implicit conversions is an antipattern and type classes would work much better for it. Actually extension methods are the only non-controversial usages of implicit conversions because even magnet pattern - the only other use case that might be used on production (see Akka) - might be seen as issue.

    So treat this example from docs as a demonstration of a mechanism and not as a example of good practice that should be used on production.