Search code examples
scalagenericsconstructorabstract-data-typealgebraic-data-types

How to call constructor for leaves of a tree algebraic data type in Scala?


I'm creating some basic abstract data types and algorithms to brush up on my CS fundamentals, and learning Scala along the way. I'm running into trouble with my BinarySearchTree data type, which is an implementation of a more abstract BinaryTree:

abstract class BinaryTree[T](stored_value: T) { 
  var contents = stored_value
  var l: this.type = _
  var r: this.type = _
  ...
}

class BinarySearchTree[T <: Ordered[T]](stored_value: T) extends BinaryTree(stored_value) {  
  def insert(newval: T) {
    if (newval <= contents) {
      if (l == null) {
        throw new NotDefinedError("Still trying to work around type erasure.")
      } else {
        l.insert(newval)
      }
    } else {
      if (r == null) {
        throw new NotDefinedError("Still trying to work around type erasure.")
      } else {
        r.insert(newval)
      }
    }
  }

In the blocks that throw NotDefinedErrors, I've tried a few approaches like l = new this.type(newval), substituting this.type for BinarySearchTree[T] and anything else I can think of. Depending on how I try to express that type, I either get something like:

class type required but BinarySearchTree.this.type found

or:

type mismatch;  found   : trees.BinarySearchTree[T]  required: BinarySearchTree.this.type

Do I need to override l and r with a different type in the definition of BinarySearchTree? Or just call a different type of constructor when I'm attaching a new value to them? Or some other option?


Solution

  • I agree with @dhg's recommendation that you explore immutable tree data structures as an alternative to your current direction. However if a mutable tree is really what you need then read on ...

    Your immediate problem is that this.type in the definition of BinaryTree[T] doesn't mean what you think it means. It's actually the singleton type of the enclosing instance, ie. the type which inhabited by this and nothing else. Here's an example illustrating that,

    scala> class Foo { def self : this.type = this /* OK */ }
    defined class Foo
    
    scala> class Bar { def self : this.type = new Bar /* Does not compile */ }
    <console>:7: error: type mismatch;
     found   : Bar
     required: Bar.this.type
           class Bar { def self : this.type = new Bar /* Does not compile */ }
                                              ^
    

    This is obviously a much more specific type than the one you really want.

    To fix the problem one option would be to weaken the types of l and r to BinaryTree[T], as in @dhg's answer. However, I think that what you were originally after when using this.type was for the tree nodes to be self-typed and so refined to BinarySearchTree[T] in the subclass. You can do this as you would in Java with a recursive type bound, or you can do it with an abstract type member like so,

    abstract class BinaryTree[T] {
      type Self <: BinaryTree[T]
      var contents : T
      var l: Self = _
      var r: Self = _
    }
    
    class BinarySearchTree[T <: Ordered[T]](stored_value : T) extends BinaryTree[T] {
      type Self = BinarySearchTree[T]
        var contents = stored_value
        def insert(newval: T) {
          if (newval <= contents) {
            if (l == null) {
              new BinarySearchTree(newval)
            } else {
              l.insert(newval)
            }
          } else {
            if (r == null) {
              new BinarySearchTree(newval)
            } else {
              r.insert(newval)
            }
          }
      }
    }