1

I have the following types defined:

trait Context

trait Attribute[C <: Context]

trait AttributeDefinition[A[_ <: Context] <: Attribute[C] forSome { type C <: Context }] {

  def read[C <: Context]: A[C]

}

I want AttributeDefinition to return an Attribute parameterized by a context. Each definition applies to only one type of Attribute. Hypothetically:

class ConstantValueAttribute[C <: Context] extends Attribute[C]

object ConstantValueAttributeDefinition extends AttributeDefinition[ConstantValueAttribute] {
  override def read[C <: Context]: ConstantValueAttribute[C] = ???
}

This totally compiles fine, but i have trouble crafting method signatures that accept an unbounded AttributeDefinition. As well as creating a collection of AttributeDefinitions. All the following fail:

def def1(attribute: AttributeDefinition): Unit =  ???
def def2(attribute: AttributeDefinition[_]): Unit = ???
def def3(attribute: AttributeDefinition[_ <: Attribute[C] forSome { type C <: Context }]): Unit = ???
def def4(attribute: AttributeDefinition[A[_]] forSome { type A <: Attribute[C]; type C <: Context })

EDIT:

I am trying to find a method declaration like the above that will compile. I also am looking to figure out how to declare a collection of AttributeDefinitions.

val val1 = new mutable.ArrayBuffer[AttributeDefinition]

if that is not possible i would love to figure out how to simplify the traits above that can still capture the simple read method declaration.

aepurniet
  • 1,719
  • 16
  • 24

3 Answers3

3

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 AttributeDefinitions 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 As.

Community
  • 1
  • 1
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • looks great! using a type member *and* keeping it as a higher kinded is something i totally did not think of. as for the variance proposal, i dont quite get why `def1` and `listOfDefs` is allowed to reference an `Attribute` trait without a parameter? – aepurniet Feb 16 '18 at 16:06
  • 1
    @aepurniet Because in the second proposal, the `AttributeDefinition` is a type constructor with a higher-kinded parameter, the kind of `AttributeDefinition` is `(* -> *) -> *`. The `Attribute` has kind `* -> *`. Therefore, `AttributeDefinition[Attribute]` is well-formed type. If it were `AttributeDefinition[Attribute[Bug]]`, then `Attribute[Bug]` wouldn't have a hole into which one could plug in the type `C` supplied to the `read`-method. – Andrey Tyukin Feb 16 '18 at 17:58
1

The correct signature would be

def `def`[A[_ <: Context] <: Attribute[C] forSome { type C <: Context }](attribute: AttributeDefinition[A]) = ???

You need to use a type parameter for your function, which should have (at least) the same constraints as the one in your type.

Note: in my opinion this is a reason why you should avoid putting type constraints on the type arguments for your type, they should only be put on functions/operations instead.

mpetruska
  • 633
  • 3
  • 6
  • interesting, how would you declare a collection of definitions? like a `Seq[???]` – aepurniet Feb 12 '18 at 18:23
  • i like the point about type constraints in arguments, is there a way to modify the trait declarations to avoid the constraints, but still have a nicely declared read method? – aepurniet Feb 12 '18 at 18:25
  • 1
    How about: `trait AttributeDefinition { def read[C <: Context, A[_] <: Attribute[C]]: A[C] }`? – mpetruska Feb 13 '18 at 11:08
  • that would work too, i was hoping to avoid type parameterized methods since that declaration would not work in a collection. – aepurniet Feb 13 '18 at 15:28
  • I'm confused what you're trying to achieve, but maybe you want to look at [shapeless](https://github.com/milessabin/shapeless). – mpetruska Feb 13 '18 at 16:00
  • Im confused about how shapeless would help me here – aepurniet Feb 14 '18 at 15:11
0
trait Context

trait Attribute[+Context]

trait AttributeDefinition[Attribute] {  
    def read[Context]: Attribute 
}


class ConstantValueAttribute extends Attribute[Context]

object ConstantValueAttributeDefinition extends AttributeDefinition[ConstantValueAttribute] {
    def read[Context]: ConstantValueAttribute = ???
}

def def1[C](attribute: AttributeDefinition[C]) : Unit = ???

As a hint for simplification too.

Volty De Qua
  • 199
  • 6
  • this loses the fact that `read` returns an `Attribute` parameterized by a `Context`. I think its a lot clearer if you used single letters as type parameters so they wont be confused with types. – aepurniet Feb 14 '18 at 22:17
  • Look. I had good time playing with this. You are going to preserve all your types as soon as you implement something concrete — with [case] classes, constructors, those attributes and contexts etc etc. I am not going to play this abstract game of yours. And, hoping that the stackover's staff can get what I am saying, I want to advise other users to ask for something concrete before they invest time in their efforts to answer abstract questione with real bounties (btw already noted that practical & genuine questsion go without). – Volty De Qua Feb 15 '18 at 00:55
  • your solution specifically doesnt preserve the nice read method which was mentioned in the bounty. and this is not just abstract, i have a real problem thats more complicated than this, i just reduced it to this question. Its a minimal, complete, and verifiable example of the problem i would like to solve – aepurniet Feb 15 '18 at 18:54