Karel Horak
Karel Horak

Reputation: 1062

Scala type inference limitations

I am developing a ORM-style library on top of ReactiveMongo. Currently I've tried to implement a nested document representation - however I've been stuck with Scala's type inference for my class. I am quite new to Scala so every help is welcome.

This is my attempt:

trait MongoDocument[T <: MongoDocument[T]] {
  self: T =>

  val db: DefaultDB
  val collection: String

  var fields: List[MongoField[T,_]] = List.empty
  def apply(doc: TraversableBSONDocument): T = { // loads the content of supplied document
    fields.foreach { field =>
      doc.get(field.field).foreach(field.load(_))
    }
    this
  }
}
trait MongoMetaDocument[T <: MongoDocument[T]] extends MongoDocument[T] {
  self: T =>

  type DocType = T

  protected val clazz = this.getClass.getSuperclass
  def create: T = clazz.newInstance().asInstanceOf[T]

  def find(id: String): Future[Option[T]] = {
    db(collection).find(BSONDocument("_id" -> BSONObjectID(id)))
      .headOption().map(a => a.map(create.apply(_)))
  }
}

abstract class MongoField[Doc <: MongoDocument[Doc], T](doc: MongoDocument[Doc], val field: String) {
  var value: Option[T] = None

  def is: Option[T] = value
  def apply(in: T) { value = Some(in) }
  def unset() { value = None }

  def load(bson: BSONValue)

  doc.fields ::= this
}

class MongoInteger[Doc <: MongoDocument[Doc]](doc: Doc, field: String)
  extends MongoField[Doc, Int](doc, field) {

  def load(bson: BSONValue) { value = Try(bson.asInstanceOf[BSONNumberLike].toInt).toOption }
}

class MongoDoc[Doc <: MongoDocument[Doc], SubDocF <: MongoMetaDocument[SubDoc], SubDoc <: MongoDocument[SubDoc]](doc: Doc, field: String, meta: SubDocF)
  extends MongoField[Doc, SubDocF#DocType](doc, field) {

  def load(bson: BSONValue) {
    value = Try(bson.asInstanceOf[TraversableBSONDocument]).toOption.map { doc =>
       meta.create.apply(doc)
    }
  }
}

Assuming that I have following code:

class SubEntity extends MongoDocument[SubEntity] {
  val db = Db.get
  val collection = ""

  val field = new MongoInteger(this, "field")
}
object SubEntity extends SubEntity with MongoMetaDocument[SubEntity]

I would like to write another entity as:

class Another extends MongoDocument[Another] {
  val db = Db.get
  val collection = "test"

  val subEntity = new MongoDoc(this, "subEntity", SubEntity)
}
object Another extends Another with MongoMetaDocument[Another]

Where Db.get just returns a DefaultDB.

However Scala cannot infer the types for the MongoDoc instance even though I thought they might be inferrable (as Doc is easily inferred, SubDocF can be inferred to SubEntity.type and as SubEntity mixins just MongoMetaDocument[SubEntity] then the SubDoc type has to be SubEntity). If I use following code, everything goes just fine:

val subEntity = new MongoDoc[Another,SubEntity.type,SubEntity](this, "subEntity", SubEntity)

Is there some solution that the types need not be set explicitly? As I need to build an instance of a class that extends from MongoDocument trait, I was trying to solve that by using the metaobject that has the create method.

Currently the only workaround I've come up with makes use of implicitly but that would make the entity definition a bit more nasty.

Thank you for helping me to sort this out (or for giving me some hint how to arrange my class hierarchy that this won't be a problem)

Upvotes: 3

Views: 1145

Answers (1)

stew
stew

Reputation: 11366

You'll get better type inference if you get rid of the "F-bounded Polymorphism". (that is when you are finding yourself writing type arguments of the form "T <: Foo[T]"), and instead use abstract type members in the traits which are reified in the concrete classes.

You'll find that inference works here:

trait MongoDocument
  type DocType <: MongoDocument

  def apply(doc: TraversableBSONDocument): DocType = ??? // loads the content of supplied document
}
trait MongoMetaDocument extends MongoDocument {

  protected val clazz = this.getClass.getSuperclass
  def create: DocType = clazz.newInstance().asInstanceOf[DocType]

  def find(id: String): Future[Option[DocType]] = ???
}

abstract class MongoField[Doc <: MongoDocument, T](doc: MongoDocument, field: String) {
  type DocType <: MongoDocument
  var value: Option[T]

  def is: Option[T] = value
  def apply(in: T) { value = Some(in) }
  def unset() { value = None }

  def load(bson: BSONValue)

}

class MongoDoc[Doc <: MongoDocument, SubDocF <: MongoMetaDocument, SubDoc <: MongoDocument](doc: Doc, field: String, meta: SubDocF)
  extends MongoField[Doc, SubDocF#DocType](doc, field) {
  type DocType = Doc

  def load(bson: BSONValue) {
    value = Try(bson.asInstanceOf[TraversableBSONDocument]).toOption.map { doc =>
       ???
    }
  }
}

class SubEntity extends MongoDocument {
  type DocType = SubEntity
  ???
}
object SubEntity extends SubEntity with MongoMetaDocument

class Another extends MongoDocument {
  type DocType = Another
  val subEntity = new MongoDoc(this, "subEntity", SubEntity)
}
object Another extends Another with MongoMetaDocument

Upvotes: 2

Related Questions