Proposal with a type member
Essentially, what you want in both defN
-declarations and Seq
-collections of AttributeDeclaration
is a higher kinded existential type for Attribute
that is used as an argument for AttributeDeclaration
. So, ideally, you would like to have:
def def1(attrDef: AttributeDeclaration[A] forSome {
type A[C <: Context] <: Attribute[C] }): Unit = ???
However, this does not work, and this is an unresolved issue since 2015, so chances are that it won't be fixed until dotty.
Instead of trying to force the compiler to work with higher kinded existentials so that you can throw away the unnecessary type parameter, I would propose that you don't add the annoying type parameter in the first place. Consider these definitions (compiles with 2.12.4):
trait Context
trait Attribute[C <: Context]
trait AttributeDefinition {
type A[C <: Context] <: Attribute[C]
def read[C <: Context]: A[C]
}
class ConstantValueAttribute[C <: Context] extends Attribute[C]
object ConstantValueAttributeDefinition extends AttributeDefinition {
type A[C <: Context] = ConstantValueAttribute[C]
def read[C <: Context]: ConstantValueAttribute[C] = ???
}
def def1(attrDef: AttributeDefinition): Unit = ???
val collDefs: Seq[AttributeDefinition] = List(ConstantValueAttributeDefinition)
These definitions have following properties:
Context
and Attribute
are left unchanged
A
becomes a type member of AttributeDefinition
, so it does not have to be forcefully erased by an existential later
read
-signature is completely unchanged, it's literally the same as in your original code, character for character
- declaration of the simplest and most intuitive
def1
works immediately without changes (also essentially copied from your code 1:1)
- declaration and definition of collections with
AttributeDefinition
s also works just as smoothly.
Recall that you don't even lose anything if you move something from type parameter to type member, because you can always refer to the type member by name (for example, when you want to impose an additional restriction on that type member). The following minimal example is supposed to demonstrate how to impose additional restrictions on type members (vaguely related to your code, uses Context
):
trait Foo {
type Member
}
def restrictiveFoo(x: Foo { type Member <: Context }): Unit = ???
Here, you can force the argumen Foo
to have type member that derives from Context
. It would work with type Member = Context
similarly. What I wanted to demonstrate by that: it's not like type members are forever hidden inside the trait, if you introduce them. You can do with them almost all the same things as with type parameter. To summarize:
- Adding additional constraints on a (higher kinded) type member is easy
- Erasing a superfluous type parameter with higher kinded existentials doesn't seem to work at all.
So, therefore, I would advise to use a type member.
Proposal with change of variance from A
to +A
Another quick-fix to make it compile:
trait Context
trait Attribute[C <: Context]
trait AttributeDefinition[+A[X <: Context] <: Attribute[X]] {
def read[C <: Context]: A[C]
}
class ConstantValueAttribute[C <: Context] extends Attribute[C]
object ConstantValueAttributeDefinition
extends AttributeDefinition[ConstantValueAttribute] {
override def read[C <: Context]: ConstantValueAttribute[C] = ???
}
class FooValueAttribute[C <: Context] extends Attribute[C]
object FooValueAttributeDefinition
extends AttributeDefinition[FooValueAttribute] {
override def read[C <: Context]: FooValueAttribute[C] = ???
}
def def1(attrDef: AttributeDefinition[Attribute]): Unit = {}
def1(ConstantValueAttributeDefinition)
val listOfDefs: List[AttributeDefinition[Attribute]] = List(
ConstantValueAttributeDefinition,
FooValueAttributeDefinition
)
inspired by the answer to this question. Strangely enough, it's seems to be almost exactly the same constellation. It didn't solve the problem with higher kinded existentials though, instead the variance of A
has been changed to +A
, so that one could use Attribute
as least upper bound of all concrete A
s.