简体   繁体   中英

Scala - type inference of generic parameter with traits

I'm working on an small rendering engine in Scala to learn the language. The architecture of the engine is strongly based on traits, so I can add and remove parts of the pipeline as I want. Some of the traits are:

( update : Corrected some type information)

trait Composable { val parent:DisplayObject with Composite = null }
trait Composite extends Composable { val children:ArrayBuffer[DisplayObject with Composable] = ArrayBuffer() }
trait Position { var x = 0.0d; var y = 0.0d }
...

DisplayObject is an empty class that is used to compose those traits.

One of the step in my pipeline is flattening a hierarchy of each object. My first shot at this was was: ( update : I added the body)

def flatten(root:DisplayObject with Composite) : ArrayBuffer[DisplayObject] =
{
    def traverse(composite:Composite, acc:ArrayBuffer[DisplayObject])
    {
      acc += composite
      for(composable <- composite.children)
      {
        composable match {
          case com:Composite => traverse(com, acc)
          case _ => acc += composable
        }
      }
    }

    val flat = new ArrayBuffer[DisplayObject]
    traverse(root, flat)
    flat
  }
}

This is fine, however, when I'm calling this function, I'm loosing a lot of type information:

val root = new DisplayObject with Position with Composite
root.children += new DisplayObject with Composable with Position
val list = flatten(root)

The type of list is now List[DisplayObject] . I'm losing the Position information. So I thought about adding generics into the mix : ( update : added body)

  def genericFlatten[T](root:T with Composite) : ArrayBuffer[T] =
  {
    def traverse(composite:T with Composite, acc:ArrayBuffer[T])
    {
      acc += composite
      for(composable <- composite.children)
      {
        composable match {
          case com:T with Composite => traverse(com, acc)
          case composable:T with Composable => acc += composable
        }
      }
    }

    val flat = new ArrayBuffer[T]
    traverse(root, flat)
    flat
  }

However, calling this gives me this weird result : the type of the returned list is now List[DisplayObject with Position with Composite] , which is wrong, because some children of the tree (the leafs) will not have the Composite trait. I was expecting the type T to be infered to DisplayObject with Position . No?

I might be wrong but I feel a certain misconception of what the type inferer does: if you do not specify the type, the type inferer will try to figure it out, but it won't replace the type you have defined yourself.

Let's take your first shot:

def flatten(root:DisplayObject with Composite) : List[DisplayObject] 

Here you set up the return type. This method will always return a List[DisplayObject] and there is absolutely nothing to infer. The signature is perfectly complete.

Let's take your second shot:

def flatten[T](root:T with Composite) : List[T] 

Here again, no type inference. There are generics parameters, and the generic value will be checked by the compiler . You can write this method in Java, which has no type inference at all


If I have correctly interpreted your answer, you would like to flatten the elements in the children list without losing their type. Hower, if we look to the Composite trait:

trait Composite extends Composable { 
        val children:ArrayBuffer[Composable] = new ArrayBuffer[Composable] 
}

Here we have a val children that has type ArrayBuffer[Composable] , more specifically it has type ArrayBuffer[T] with generic parameter T = Composable . This is the compile type you enforce in declaration and in a static typed programming language such as Scala or Java this is not allowed to change during the execution of your program.

This the key point for understanding your problem: try to think about it in Java. If you have a List<Object> you can put inside an Integer, but that won't turn your List<Object> into a List<Integer> . Let's break your code for adding a children into two rows.

val firstChildren:DisplayObject with Position = new DisplayObject with Position
root.children += firstChildren

Here, as soon as your firstChildren val gets out of scope, you will lose its type information. If you access it through the root.children you won't know it's a DisplayObject with Position but only a Composable . Furthermore firstChildren is a DisplayObject with


Disclaimer:

* What you are trying to is not trivial however because Composable and Composite classes have a circular reference. I have been breaking it to provide some simple working code, but I have to warn you it would take you a certain amount of experience in Scala before you could master the type system well. *

You need somehow to keep the information about the children type on the parent and the information about the parent on the children. You therefore need two type parameters.

  trait Composable[K,T<:Composite[K,T]] {
    val parent:T
  }

  trait Composite[K,T<:Composite[K,T]] extends Composable[K,T] {
    val children:ArrayBuffer[K] = new ArrayBuffer[K]
  }

  trait Position { val x = 0.0d; val y = 0.0d }

  class DisplayObject

  def flatten[K,T<:Composite[K,T]](root:DisplayObject with Composite[K,T]) : List[K] =
  {
    root.children.toList
  }
  class ComposableDisplayObjectWithPosition extends DisplayObject with Position with        Composite[DisplayObject with Position,ComposableDisplayObjectWithPosition]{
      // dangerous
      val parent = this
    }

  def main(args:Array[String]){

    val root = new ComposableDisplayObjectWithPosition
    root.children += new DisplayObject with Position
    val list:List[DisplayObject with Position] = flatten(root)
    println(list)
  }

Disclaimer: I am no Scala expert, the argument below is generic (pardon thee pun).

The signature func(param:T with X) means that if the actual parameter of func is of type A , then it is required that A<:T and A<:X . These requirements are totally separate.

There's no without in the type system. If T is generic, it is not inferred to be A without X , there's no such thing. T is inferred to be A , as it's the only easiest and most useful way to satisfy A<:T . A<:X is checked separately.

If you pass DisplayObject with Position with Composite to flatten , you get back DisplayObject with Position with Composite . If you pass Foo with Bar with Composite , you get that back too. You can't subtract Composite , since there's no such thing as without . I don't know why anyone would want to. I cannot imagine a situation where extra type information would hurt.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM