Type class recursive resolution

In our previous example, we haven't implemented the connection functionality for the parameterized UsbC type, and our solution was only limited to UsbC[String].

We could improve our solution by further delegating the connection logic. Say that we have an implicit function, T => Boolean, available—we could say that this is the logic the user of our library wants to use to describe the connection method. 

This is an example of a bad use of implicits. This does not only include primitive Boolean types; it is highly probable that it will refer to another predefined type at the moment the implicit conversion will be defined. We provide this example exactly as it is mentioned—as an illustration of the bad design to be avoided!

This is what our delegate method could look like:

implicit def usbCCableDelegate[T](implicit conn: T => Boolean): Cable[UsbC[T]] = (c: UsbC[T]) => conn(c.kind)

It literally reflects the intuition we had for the delegating function—the compiler will create an instance of Cable[UsbC[T]] if there is an implicit conversion of T => Boolean available.

This is how this could be used:

implicit val symbolConnect: Symbol => Boolean = 
(_: Symbol).name.toLowerCase.contains("cable")

scala> connectCable(UsbC('NonameCable))
res18: Boolean = true
scala> connectCable(UsbC('FakeKable))
res19: Boolean = false

But then, we have to deal with all of the dangers of the implicit conversion we're delegating to. For example, having the following unrelated conversions in scope:

implicit val isEven: Int => Boolean = i => i % 2 == 0
implicit val hexChar: Char => Boolean = c => c >= 'A' && c <='F'

Would suddenly allow us to connect cables in unexpected ways:

scala> connectCable(UsbC(10))
res23: Boolean = true
scala> connectCable(UsbC(11))
res24: Boolean = false
scala> connectCable(UsbC('D'))
res25: Boolean = true

It might look like it is a dangerous approach to have an implicit definition that relies on the existence of another implicit in order to produce the required implicit value, but this is exactly what gives type classes their power.

To demonstrate this, let's imagine that we'd like to implement a USB adapter that should connect two USB devices with different standards. We could do this easily by representing our adapter as a pair of cable ends to connect, and delegating the real connection to the respective end of the cable:

implicit def adapt[A, B](implicit ev1: Cable[A], ev2: Cable[B]): Cable[(A, B)] = new Cable[(A, B)] {
def connect(ab: (A, B)): Boolean =
ev1.connect(ab._1) && ev2.connect(ab._2)
}

Or, we could use context bounds and SAM syntax: 

implicit def adapt[A: Cable, B: Cable]: Cable[(A, B)] =
(ab: (A, B)) =>
implicitly[Cable[A]].connect(ab._1) &&
implicitly[Cable[B]].connect(ab._2)

Now, we can use this implicit to call our existing connectCable method, but with the adapter logic:

scala> val usb2usbC = (Usb(false), UsbC('NonameCable))
usb2usbC: (Usb, UsbC[Symbol]) = (Usb(false),UsbC('NonameCable))

scala> connectCable(usb2usbC)
res33: Boolean = false

scala> val lightning2usbC = (Lightning(150), UsbC('NonameCable))
lightning2usbC: (Lightning, UsbC[Symbol]) = (Lightning(150),UsbC('NonameCable))

scala> connectCable(lightning2usbC)
res34: Boolean = true

Impressive, isn't it? Just imagine how much effort would be needed to add this functionality to the OO version!

The fun does not stop here! Because of the recursive nature of the context bound resolution, we can now build a chain of any length and the compiler will recursively check if it is possible to build the required adapter at compile time:

scala> val usbC2usb2lightning2usbC = ((UsbC('NonameCable), Usb(false)), (Lightning(150), UsbC("USB 3.1")))
usbC2usb2lightning2usbC: ((UsbC[Symbol], Usb), (Lightning, UsbC[String])) = ((UsbC('NonameCable),Usb(false)),(Lightning(150),UsbC(USB 3.1)))

scala> connectCable(usbC2usb2lightning2usbC)
res35: Boolean = false

scala> val noUsbC_Long_Cable = (UsbC('NonameCable), (Lightning(150), UsbC(10L)))
noUsbC_Long_Cable: (UsbC[Symbol], (Lightning, UsbC[Long])) = (UsbC('NonameCable),(Lightning(150),UsbC(10)))

scala> connectCable(noUsbC_Long_Cable)
^
error: could not find implicit value for evidence parameter of type Cable[(UsbC[Symbol], (Lightning, UsbC[Long]))]

We can improve the error message a bit by applying a special annotation on our type class definition:

@scala.annotation.implicitNotFound("Cannot connect cable of type ${C}")
trait Cable[C] {
def connect(c: C): Boolean
}

Then, our last unsuccessful attempt will explain the reason for the failure a bit better:

scala> connectCable(noUsbC_Long_Cable)
^
error: Cannot connect cable of type (UsbC[Symbol], (Lightning, UsbC[Long]))

Unfortunately, this is only as far as we can get in this case. The compiler can't currently figure out that the real reason for the failure is just UsbC[Long] and not the whole type.

The compiler will always try to infer the most specific implicit value with respect to subtyping and variance. This is why it is possible to combine subtype polymorphism and ad-hoc polymorphism.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.20.231