Note
Is this a bug? https://scastie.scala-lang.org/Q6b2u2YSQIy8IlSXchqy8g
Or is there some other way to stop from exporting multiple members of a delegatee? I had assumed it was export foo.{a => _, b => _, c => _, _}
but that gives an error. Iâm going to assume that that is the correct syntax to blacklist multiple members for the rest of this comment. If there is actually no way to blacklist multiple members of an object from export, then I think that should be considered a really serious problem with the proposal.
AnywaysâŚ
I donât really have any opinion on how the exporting works for the package object use-cases, but I would like to point out some ways that exports compare unfavourably to Kotlinâs delegating functionalty in the usecase of creating delegating wrappers.
Exporting only the members that conform to an interface
Dottyâs export foo._
will very often in everyday programming export too many methods, and in most delegating cases, we will want to only export the members of foo
which conform to an interface rather than the concrete implementation. However, the only way to do so is to couple the delegator to either the interface members or to the delegateeâs concrete members.
Example
trait MyTrait {
def a: Int
def b: Int
def c: Int
}
class MyImplementation extends MyTrait {
def a: Int = 1
def b: Int = 2
def c: Int = 3
def d: Int = 4
def e: Int = 5
def f: Int = 6
}
class Delegating(underlying: MyImplementation) extends MyTrait {
// ??? what to export here?
}
We cannot do:
export underlying._
because that will export members d, e, f
. So we must do either:
A)
export underlying.{d => _, e => _, f => _, _}
or B)
export underlying.{a, b, c}
Both are bad for similar reasons.
The A) solution forces all use-sites to directly couple against the members of MyImplementation
. What happens when MyImplementation
gets a new member added? Delegating
will quitely have new members added to it, without warning. Or what happens when a member is removed from MyImplementation
? Delegating
will have to be modified to no longer blacklist any dropped members. Both of these are really prickly because as a delegator we donât want to necessarily keep track of every single one of the members exposed by an implementation.
The B) solution forces all use-sites to directly couple against members of MyTrait
. For so the same thing applies. When MyTrait
has members added or removed, all delegators must change their own source, even thought their intention has not changed.
Also note that A) will become unwieldy if there becomes too many members of MyImplementation
, and B) will become unwieldy if there becomes too many members of MyTrait
Kotlin solves this problem in a much more elegant way IMO
interface MyInterface {
fun a(): Unit
fun b(): Unit
fun c(): Unit
}
class MyImplementation: MyInterface {
override fun a(): Unit {}
override fun b(): Unit {}
override fun c(): Unit {}
fun d(): Unit {}
fun e(): Unit {}
fun f(): Unit {}
}
class Delegating(private val underlying: MyImplementation): MyInterface by underlying
Here the delegator can clearly specify, âI am only exporting members of MyImplementation which are implementations of MyInterfaceâ. I donât have to go look at the MyImplementation source code and its entire inheritance hierarchy to figure out which members it has, I donât have to check to make sure that that potentially very large list of members doesnât change over time. And I donât have to do the same thing for MyInterface either.
The way I see it, this problem alone is enough to consider the proposed delegating functionality in Dotty as only working in the most simple of cases.
Overloading
The above solutions to the problem of delegating, even with their issues, donât seem to work in the presence of overloaded members:
trait MyTrait {
def a(i: Int): Int
}
class MyClass extends MyTrait{
def a(i: Int): Int = 1
def a(i: Int, j: Int): Int = 2
}
class Delegating(myClass: MyClass) extends MyTrait {
export myClass.a
}
@main def main(): Unit = {
println(Delegating(MyClass()).a(1,2)) // 2
}
To my knowledge, there isnât a way in this scenario to export only the a
that corresponds to MyTrait
and not the one which corresponds to MyClass, without creating an other field:
class Delegating(myClass: MyClass) extends MyTrait {
private[this] val myTrait: MyTrait = myClass
export myTrait.a
}
This workaround gets the job done but at the cost of an additional field which will have runtime costs, and it just seems like a feature designed expressly for the purposes of encouraging delegation shouldnât require these sorts of workarounds.