If we can, I’d like to avoid getting into a discussion about what defines delegation precisely. The usecase I’m trying to evaluate exports against is the example given in the proposal, the one about printers and scanners. My thought is that the example given is a bit too simplistic to be really indicative of how the feature would be used in the real world, since it only is concerning 3 tiny concrete classes interacting with each other. In the real world, we are most often producing and consuming interfaces rather than grab bags of individual members, yet also need to be able to avoid delegating specific members. So a more realistic version of the example might look like this:
class BitMap
class InkJet
trait Printer {
type PrinterType
def print(bits: BitMap): Unit
def print(bits: BitMap, n: Int): Unit
def print(bits: BitMap, colour: String): Unit
def printDiagnostics(): Unit
def tunOnPrinter(): Unit
def turnOffPrinter(): Unit
def status: List[String]
}
class PrinterImpl extends Printer {
def print(bits: BitMap): Unit = ???
def print(bits: BitMap, n: Int): Unit = (0 until n).foreach(_ => print(bits))
def print(bits: BitMap, colour: String): Unit = ???
def printDiagnostics(): Unit = ???
def tunOnPrinter(): Unit = ???
def turnOffPrinter(): Unit = ???
def status: List[String] = ???
def otherPrinterImplMethod(): Unit = ()
}
trait Scanner {
def scan(): BitMap
def scan(n: Int): Seq[BitMap]
def scanColour(): BitMap
def scanColour(n: Int): Seq[BitMap] = (0 until n).map(_ => scanColour())
def status: List[String]
}
class ScannerImpl extends Scanner {
def scan(): BitMap = ???
def scan(n: Int): Seq[BitMap] = (0 until n).map(_ => scan())
def scanColour(): BitMap = ???
def status: List[String] = ???
def otherScannerImplMethod(): Unit = ()
}
class Copier(private val printUnit: PrinterImpl, private val scannerUnit: ScannerImpl) extends Printer with Scanner {
// problem 1: The fact that we use a PrinterImpl and ScannerImpl is an implementation detail. We don't want to
// accidentally leak members when exporting, so we must explicitly up-cast into the interface we want to
// export. This will also have runtime consequences as Copier must now hold 2 extra references at runtime!
private[this] val printer: Printer = printUnit
private[this] val scanner: Scanner = scannerUnit
// problem 2: Every new method that printer gets, I must modify my code here, why do I have to type all
// this out? This will result in a lot of code churn as every time any of my delegatees are changed,
// I must change too
// I could instead blacklist, but then I have the same problem in reverse, having to call out twice
// which methods I want to avoid delegating
// Problem 3: if someone is reading this code later, is it immediately obvious why I'm exporting these methods?
// or why I'm exporting the scanner method? I must manually guess and check which traits are covered by
// which exports
export printer.{printDiagnostics, tunOnPrinter, turnOffPrinter}
export scanner.{status => _, _}
// problem 4: can only export/blacklist by name, so if there are overloads, exports don't work!
// here, we only care to "override" the first print overload, but that forces us to wire up all
// other overloads.
def print(bits: BitMap): Unit = {
println("printing!")
printer.print(bits)
}
def print(bits: BitMap, n: Int): Unit = printer.print(bits, n)
def print(bits: BitMap, colour: String): Unit = printer.print(bits, colour)
def otherCopierMethod(): Unit = {
printUnit.otherPrinterImplMethod()
scannerUnit.otherScannerImplMethod()
}
def status: List[String] = printer.status ++ scanner.status
}
I have listed ~4 problems that come up when you try to increase complexity from tiny to medium-small. These add up to a lot of complexity and mental burden when trying to use exports in a real world setting.
My prediction is that, since it is a headache to expose a conservative interface people will end up not paying much attention to it and just throwing out export foo._
everywhere. This would lead to unnecessarily tight coupling. In the example above, we would end up exporting otherPrinterImplMethod
and otherScannerImplMethod
simply because that’s easier to do / requires less thinking than upcasting.
If instead we take inspiration from Kotlin’s delegates, doing “the right thing” is simple and easy:
class Copier(private val printUnit: PrinterImpl, private val scannerUnit: ScannerImpl)
extends Printer by printUnit with Scanner by scannerUnit {
override def print(bits: BitMap): Unit = {
println("printing!")
printer.print(bits)
}
def otherCopierMethod(): Unit = {
printUnit.otherPrinterImplMethod()
scannerUnit.otherScannerImplMethod()
}
}