2

I'm writing a trait to describe node-like objects in a Hierarchical structure (like a graph). My working code is below:

trait Hierarchical[T <: Hierarchical[_]] {
  // The parents will be a list of some arbitrary Hierarchical
  val parents: List[Hierarchical[_]] = Nil
  var children: List[Hierarchical[T]] = List()

  def addChild(child: Hierarchical[T]): Unit = {
    children ++= List(child)
  }
}

abstract class NonRootNode(override val parents: List[Hierarchical[_]])
  extends Hierarchical[NonRootNode] {
}

abstract class RootNode extends Hierarchical[NonRootNode] {
  final override val parents = Nil
}

I have some tests which reflect this behavior:

import org.scalatest.FlatSpec

class DummyRoot extends RootNode
class DummyNonRoot(override val parents: List[DummyRoot]) 
  extends NonRootNode(parents)

class TestUtil extends FlatSpec {

  "A RootNode" should "not have parents" in {
    val dummyRoot = new DummyRoot
    assert(dummyRoot.parents.isEmpty)
  }

  "A NonRootNode" should "have parents when constructed with them" in {
    val dummyRoot = new DummyRoot
    assert(dummyRoot.parents.isEmpty)
    val dummyNonRoot = new DummyNonRoot(List(dummyRoot))
    dummyRoot.addChild(dummyNonRoot)
    assert(dummyNonRoot.parents.contains(dummyRoot))
    assert(dummyRoot.children.contains(dummyNonRoot))
  }    
}

However, I find the API to be a bit unwieldy in the second test. I shouldn't need to add a child to the root node explicitly since I've already specified the child's parents. I'd like to remove this from the public API, so my thought is to modify NonRootNode's constructor behavior to call this for each of the parents. Specifically, I want to write:

abstract class NonRootNode(override val parents: List[Hierarchical[_]])
  extends Hierarchical[NonRootNode] {

  //for each parent, add `this` to its children
  parents.map{p=>p.addChild(this)}
}

However, when I add this line, I get the following error:

Error:(19, 29) type mismatch;
 found   : NonRootNode
 required: Hierarchical[_$3]
  parents.map{p=>p.addChild(this)}

I'm not entirely sure why I'm getting this compiler error. My understanding of Hierarchical[_] is any Hierarchical, but I could be mistaken. In any case, I think I'm close to my desired behavior. What am I doing wrong?

erip
  • 16,374
  • 11
  • 66
  • 121
  • How can the parents be any `Hierarchical` while the children are `Hierarchical[T]`? – Jasper-M Nov 06 '16 at 15:20
  • @Jasper-M Because parents can be `RootNode`s or `NonRootNode`s (or any other extension of `Hierarchical`). Children will all be of the same type such that they are `Hierarchical`. – erip Nov 06 '16 at 15:21

1 Answers1

3

Hierarchical[_] does NOT mean "any Hierarchical". It means "a Hierarchical of some fixed type, that is unknown".

For example

def foo: Hierarchical[_] = new Hierarchical[Int] 

works: it declares a function, that will return some implementation of Hierarchical, exact type of which is not known to the caller. This is fine.

On the other hand:

def bar(h: Hierarchical[String]) = doStuff(h)
bar(foo)

Does not work: function bar wants a parameter of an exact type Hierarchical[String], and what is passed to it can not be guaranteed to have that type, it's type parameter is unknown.

In your case, .addChild is a method of Hierarchical[T] (where the value of T is unknown to you), and wants a parameter of the same type . But what you are passing to it is Hierarchical[NonRootNode]. This is illegal, because there is no way to guarantee that NonRootNode will be the same as the (unknown) T.

Dima
  • 39,570
  • 6
  • 44
  • 70
  • Interesting -- makes sense. Is there an easy way to achieve my behavior? I've tried adding an extra type parameter, but found myself in a mutual recursion situation, which became a bit cumbersome. – erip Nov 06 '16 at 15:29
  • 1
    Well, I am not sure, what exactly you actually mean by "my behavior" here. Your example is not sufficient to deduce that, because, judging by it, there is no reason for the `Hierarchical` to be parametrized at all :/ – Dima Nov 06 '16 at 15:46
  • 1
    So that may be the answer -- parameterization is unnecessary. Will update tests and report back soon. – erip Nov 06 '16 at 15:58
  • And indeed there is no need for parameterization. Thanks a lot! – erip Nov 06 '16 at 16:08