How can I chain implicits in Scala?
Asked Answered
A

3

78

The pimp-my-library pattern allows me to seemingly add a method to a class by making available an implicit conversion from that class to one that implements the method.

Scala does not allow two such implicit conversions taking place, however, so I cannot got from A to C using an implicit A to B and another implicit B to C. Is there a way around this restriction?

Attire answered 16/3, 2011 at 22:52 Comment(1)
@ryeguy Here's a meta question for the pimp/enrich debate, because holy crap that tag. That tag...Conjugated
A
109

Scala has a restriction on automatic conversions to add a method, which is that it won't apply more than one conversion in trying to find methods. For example:

class A(val n: Int)
class B(val m: Int, val n: Int)
class C(val m: Int, val n: Int, val o: Int) {
  def total = m + n + o
}

// This demonstrates implicit conversion chaining restrictions
object T1 { // to make it easy to test on REPL
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB(a: A): B = new B(a.n, a.n)
  implicit def bToC(b: B): C = new C(b.m, b.n, b.m + b.n)

  // won't work
  println(5.total)
  println(new A(5).total)

  // works
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

EDIT: View bounds ('<%') are deprecated since Scala 2.11 https://issues.scala-lang.org/browse/SI-7629 (You can use type classes instead)

However, if an implicit definition requires an implicit parameter itself(View bound), Scala will look for additional implicit values for as long as needed. Continue from the last example:

// def m[A <% B](m: A) is the same thing as
// def m[A](m: A)(implicit ev: A => B)

object T2 {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB[A1 <% A](a: A1): B = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1): C = new C(b.m, b.n, b.m + b.n)

  // works
  println(5.total)
  println(new A(5).total)
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

"Magic!", you might say. Not so. Here is how the compiler would translate each one:

object T1Translated {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB(a: A): B = new B(a.n, a.n)
  implicit def bToC(b: B): C = new C(b.m, b.n, b.m + b.n)

  // Scala won't do this
  println(bToC(aToB(toA(5))).total)
  println(bToC(aToB(new A(5))).total)

  // Just this
  println(bToC(new B(5, 5)).total)

  // No implicits required
  println(new C(5, 5, 10).total)
}

object T2Translated {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB[A1 <% A](a: A1): B = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1): C = new C(b.m, b.n, b.m + b.n)

  // Scala does this
  println(bToC(5)(x => aToB(x)(y => toA(y))).total)
  println(bToC(new A(5))(x => aToB(x)(identity)).total)      
  println(bToC(new B(5, 5))(identity).total)

  // no implicits required
  println(new C(5, 5, 10).total)
}

So, while bToC is being used as an implicit conversion, aToB and toA are being passed as implicit parameters, instead of being chained as implicit conversions.

EDIT

Related question of interest:

Attire answered 16/3, 2011 at 22:52 Comment(10)
Nice explanation. The stated reason for disallowing implicit conversion chaining is to avoid complexity and debugging nightmare. I wonder why is then chaining allowed for implicit parameters?Superfluity
Nice! I learned something new. This should be on the "hidden features" page.Watermelon
Thanks! One minor issue: I had to add add explicit result types to the functions aToB and bToC in T2 when I tried it in REPL.Astrogeology
@Astrogeology It won't be necessary on the upcoming 2.9, but I have modified the code to make it compatible with 2.8. Thanks.Attire
Just a note that is the chaining you're trying to do involves higher kinded types, then again type inference can hose you. I.e. I have M[A]. I have an implicit A=>B and an implicit M[] => N[], where both M and N are monadic. I want to create an N[B] using the two conversions. Chaining these requires an additional method call, the first to capture M[_] and the second to capture A.Birgit
@Birgit Yeah, that can be tricky. I'll see if I can work out a reasonable short and understandable example.Attire
@Peter Better to ask a question with that specific problem. I think adding it here would stray off topic.Attire
Thanks @DanielC.Sobral! Terrific answer, really clarified things for me.Debag
@DanielC.Sobral That's a great answer! After reading it a few times, it doesn't look like crazy voodoo anymore.Vying
Without view bounds: docs.scala-lang.org/tutorials/FAQ/chaining-implicits.html (see comment in the bottom)Embroidery
E
12

Note that you can build circles with implicit parameters, too. Those are, however, detected by the compiler, as exhibited by this:

class Wrap {
  class A(implicit b : B)
  class B(implicit c : C)
  class C(implicit a : A)

  implicit def c = new C
  implicit def b = new B
  implicit def a = new A
}

The error(s) given to the user are not as clear as they could be, though; it just complains could not find implicit value for parameter for all three construction site. That might obscure the underlying problem in less obvious cases.

Experiential answered 17/3, 2011 at 13:15 Comment(0)
E
1

Here's a code that also accumulates the path.

import scala.language.implicitConversions

// Vertices
case class A(l: List[Char])
case class B(l: List[Char])
case class C(l: List[Char])
case class D(l: List[Char])
case class E(l: List[Char])

// Edges
implicit def ad[A1 <% A](x: A1) = D(x.l :+ 'A')
implicit def bc[B1 <% B](x: B1) = C(x.l :+ 'B')
implicit def ce[C1 <% C](x: C1) = E(x.l :+ 'C')
implicit def ea[E1 <% E](x: E1) = A(x.l :+ 'E')

def pathFrom(end:D) = end

pathFrom(B(Nil))   // res0: D = D(List(B, C, E, A))
Eighteenth answered 11/3, 2013 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.