Search code examples
scalaabstract-classtype-parameter

Define function for extension of abstract class


I'm having trouble with type mismatches when trying to write a function that takes as input (and output) an object that extends an abstract class.

Here is my abstract class:

abstract class Agent {
  type geneType
  var genome: Array[geneType]
}

Here is my function:

def slice[T <: Agent](parentA: T, parentB: T):(T, T) = {
  val genomeSize = parentA.genome.length

  // Initialize children as identical to parents at first. 
  val childA = parentA
  val childB = parentB

  // the value 'index' is sampled randomly between 0 and 
  // the length of the genome, less 1.  
  // This code omitted for simplicity. 
  val index;
  val pAslice1 = parentA.genome.slice(0, index + 1)
  val pBslice1 = parentB.genome.slice(index + 1, genomeSize)
  val genomeA = Array.concat(pAslice1, pBslice1)
  childA.genome = genomeA

  // And similary for childB. 
  // ...
  // ...

  return (childA, childB)
}

I'm receiving an error (I'm running this with sbt, by the way) as follows:

[error] ..........  type mismatch;
[error]  found   : Array[parentA.geneType]
[error]  required: Array[T#geneType]

I'm not sure what the problem is, as I'm new to abstract classes, generic type parametrization, and probably other relevant concepts whose names I don't know.


Solution

  • In your construction it is well possible that parentA and parentB are different types, T only gives you an upper bound (they must be at least as specific as T). Arrays are invariant in their element type, thus you cannot exchange the elements in a sound way here.

    A second problem with your code is that you are returning objects of type T, but actually you are mutating the input arguments. Either you want mutation, then declare the method's return type Unit to make that clear; or create new instances of T and make Agent immutable. It depends on your performance requirements, but I would always try the immutable variant first, because it is easier to reason about.

    Here is mutable variant. Note that because arrays are special objects on the JVM (no type erasure happening), you need to provide a so-called class-tag for them as well:

    abstract class Agent {
      type geneType
      var genome: Array[geneType]
      implicit def geneTag: reflect.ClassTag[geneType]
    }
    
    def slice[A](parentA: Agent { type geneType = A }, 
                 parentB: Agent { type geneType = A }): Unit = {
      val genomeSize = parentA.genome.length
      require (parentB.genome.length == genomeSize)
      import parentA.geneTag
    
      val index    = (math.random * genomeSize + 0.5).toInt
      val (aInit, aTail) = parentA.genome.splitAt(index)
      val (bInit, bTail) = parentB.genome.splitAt(index)
      val genomeA  = Array.concat(aInit, bTail)
      val genomeB  = Array.concat(bInit, aTail)
      parentA.genome = genomeA
      parentB.genome = genomeB
    }
    

    Here you require that parentA and parentB share an exactly defined gene-type A. You can define a type alias to simplify specifying that type:

    type AgentT[A] = Agent { type geneType = A }
    
    def slice[A](parentA: AgentT[A], parentB: AgentT[A]): Unit = ...
    

    To preserve the parents and create new children, the easiest would be to add a copy method to the Agent class:

    abstract class Agent {
      type geneType
      var genome: Array[geneType]
      implicit def geneTag: reflect.ClassTag[geneType]
    
      def copy(newGenome: Array[geneType]): AgentT[geneType]
    }
    
    type AgentT[A] = Agent { type geneType = A }
    
    def slice[A](parentA: AgentT[A], parentB: AgentT[A]): (AgentT[A], AgentT[A]) = {
      val genomeSize = parentA.genome.length
      require (parentB.genome.length == genomeSize)
      import parentA.geneTag
    
      val index    = (math.random * genomeSize + 0.5).toInt
      val (aInit, aTail) = parentA.genome.splitAt(index)
      val (bInit, bTail) = parentB.genome.splitAt(index)
      val genomeA  = Array.concat(aInit, bTail)
      val genomeB  = Array.concat(bInit, aTail)
      (parentA.copy(genomeA), parentB.copy(genomeB))
    }
    

    If you don't need to squeeze the last bits of performance, you could use an immutable collection such as Vector instead of Array.

    case class Agent[A](genome: Vector[A]) {
      def size = genome.size
    }
    
    def slice[A](parentA: Agent[A], parentB: Agent[A]): (Agent[A], Agent[A]) = {
      val genomeSize = parentA.size
      require (parentB.size == genomeSize)
    
      val index    = (math.random * genomeSize + 0.5).toInt
      val (aInit, aTail) = parentA.genome.splitAt(index)
      val (bInit, bTail) = parentB.genome.splitAt(index)
      val genomeA  = aInit ++ bTail
      val genomeB  = bInit ++ aTail
      (parentA.copy(genomeA), parentB.copy(genomeB))
    }