Search code examples
scalatypes

Solving method overloading error in scala with the type system


I have some code that compares a document with another and outputs pointers to fields that have changed, with the old and new values.

Some elements of the document have a sequenceId captured by this trait:

trait ExplicitlySequencedObject {
  val sequenceId: Int
}

Elements in the document that can be compared also extend a utility trait called DiffTools which includes the following method signatures:

trait DiffTools[T] {
def createDiff[E <: DiffTools[E]](original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff

def createDiff[E <: DiffTools[E]](original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff
}

Some elements are not explicitly sequenced, but where they are I want createDiff to work differently from this implementation, so method signatures like the following are added to DiffTools in addition to the above:

  def createDiff[S <: DiffTools[S] with ExplicitlySequencedObject](
    original: Seq[S],
    current: Seq[S],
    pointerString: DocumentFieldPointer
  ): DocumentDiff

  def createDiff[S <: DiffTools[S] with ExplicitlySequencedObject](
    original: Option[Seq[S]],
    current: Option[Seq[S]],
    pointerString: DocumentFieldPointer
  ): DocumentDiff

When I do this I get a compiler error method createDiff is defined twice and I'm guessing this is related to type erasure? I'd like to know why exactly this doesn't work.

Anyway I've solved the problem for now by just renaming the method (KISS, right?) but I'm thinking this is not the best scala has to offer in solving this with its fancy type system, so other approaches would be appreciated.


Solution

  • For example you can add one more type parameter

    trait DiffTools[T, Upper] {
      def createDiff[E <: DiffTools[E, Upper] with Upper](original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff
      def createDiff[E <: DiffTools[E, Upper] with Upper](original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff
    }
    

    and have implementations of two sorts

    class DiffToolsImpl[T] extends DiffTools[T, Any] {
      override def createDiff[E <: DiffTools[E, Any]](original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff = ???
      override def createDiff[E <: DiffTools[E, Any]](original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff = ???
    }
    
    class ExplicitlySequencedDiffToolsImpl[T] extends DiffTools[T, ExplicitlySequencedObject] {
      override def createDiff[S <: DiffTools[S, ExplicitlySequencedObject] with ExplicitlySequencedObject](
        original: Seq[S],
        current: Seq[S],
        pointerString: DocumentFieldPointer
      ): DocumentDiff = ???
    
      override def createDiff[S <: DiffTools[S, ExplicitlySequencedObject] with ExplicitlySequencedObject](
        original: Option[Seq[S]],
        current: Option[Seq[S]],
        pointerString: DocumentFieldPointer
      ): DocumentDiff = ???
    }
    

    Also you could comsider to replace F-bounds and overloading (one flavor of ad hoc polymorhism) with type classes (another flavor of ad hoc polymorphism)

    http://tpolecat.github.io/2015/04/29/f-bounds.html

    Something like

    trait DiffTools[A] {
      def createDiff(original: A, current: A, pointerString: DocumentFieldPointer): DocumentDiff
    }
    trait LowPriorityDiffTools {
      implicit def seq[E]: DiffTools[Seq[E]] = new DiffTools[Seq[E]] {
        override def createDiff(original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff = ???
      }
      implicit def optSeq[E]: DiffTools[Option[Seq[E]]] = new DiffTools[Option[Seq[E]]] {
        override def createDiff(original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff = ???
      }
    }
    object DiffTools extends LowPriorityDiffTools {
      implicit def explicitSeq[E <: ExplicitlySequencedObject]: DiffTools[Seq[E]] = new DiffTools[Seq[E]] {
        override def createDiff(original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff = ???
      }
      implicit def explicitOptSeq[E <: ExplicitlySequencedObject]: DiffTools[Option[Seq[E]]] = new DiffTools[Option[Seq[E]]] {
        override def createDiff(original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff = ???
      }
    
      def createDiff[A](original: A, current: A, pointerString: DocumentFieldPointer)(implicit diffTools: DiffTools[A]): DocumentDiff =
        diffTools.createDiff(original, current, pointerString)
    }
    
    class ExplicitlySequencedObjectImpl extends ExplicitlySequencedObject {
      override val sequenceId: Int = 100
    }
    
    val opt: Option[Seq[ExplicitlySequencedObjectImpl]] = Some(Seq(new ExplicitlySequencedObjectImpl()))
    DiffTools.createDiff(opt, opt, new DocumentFieldPointer {})
    

    Type classes would be the most flexible approach.

    The minimal change to your original code would be DummyImplicit, @LuisMiguelMejíaSuárez is correct

    trait DiffTools[T] {
      def createDiff[E <: DiffTools[E]](original: Seq[E], current: Seq[E], pointerString: DocumentFieldPointer): DocumentDiff
    
      def createDiff[E <: DiffTools[E]](original: Option[Seq[E]], current: Option[Seq[E]], pointerString: DocumentFieldPointer): DocumentDiff
    
      def createDiff[S <: DiffTools[S] with ExplicitlySequencedObject](
        original: Seq[S],
        current: Seq[S],
        pointerString: DocumentFieldPointer
      )(implicit dummyImplicit: DummyImplicit): DocumentDiff
    
      def createDiff[S <: DiffTools[S] with ExplicitlySequencedObject](
        original: Option[Seq[S]],
        current: Option[Seq[S]],
        pointerString: DocumentFieldPointer
      )(implicit dummyImplicit: DummyImplicit): DocumentDiff
    }