Search code examples
scalaimplicit-conversionstructural-typing

How to use implicit conversion instead of structural typing when only one collection method is needed


I have a method in which I only need to access one method (appended, or :+) of a type. I want to be able to use both a custom case class and a normal List. Coming from a TypeScript background, I know how to do this using structural typing:

case class CustomContainer[T](data: List[T]) {
  def :+(other: T): CustomContainer[T] = copy(data = data :+ other)
}

def append[T, A <: { def :+(other: T): A }](appendable: A, other: T): A = appendable :+ other

append(List(1,2,3), 4) // List(1,2,3,4)
append(CustomContainer(List(1,2,3)), 4) // CustomContainer(List(1,2,3,4))

However, I also know there are performance and maintainability disadvantages to using structural types. I'd like to find a better way, so I'm trying to do this using traits instead, but the sticking point is that I can't make List extend some trait that I defined. I have a feeling the answer is implicit conversion, but I can't figure out how to implicitly convert something to a trait, which is an abstract type.

Here's what I've tried so far:

trait Appendable[T, C] {
  def :+(other: T): C
}

case class CustomContainer[T](data: List[T]) extends Appendable[T, CustomContainer[T]] {
  override def :+(other: T): CustomContainer[T] = copy(data = data :+ other)
}

implicit def listToAppendable[T](list: List[T]): Appendable[T, List[T]] = ??? // What goes here?

def append[T, A <: Appendable[T, A]](appendable: A, other: T): A = appendable :+ other

append(List(1,2,3), 4) // List(1,2,3,4)
append(CustomContainer(List(1,2,3)), 4) // CustomContainer(List(1,2,3,4))

What goes in the ??? spot? Alternatively is there a better way to do this?

I am on Scala 2.10.4.


Solution

  • You can use a type class for this:

    final case class CustomContainer[T](data: List[T])
    
    trait Appendable[T, C[_]] {
      def specialAppend(other: T, acc: C[T]): C[T]
    }
    object Appendable {
      implicit def listAppendable[A]: Appendable[A, List] = new Appendable[A, List] {
         override def specialAppend(other: A, acc: List[A]): List[A] = other :: acc
      }
    
      implicit def customContainerAppendable[A]: Appendable[A, CustomContainer] = new Appendable[A, CustomContainer] {
        override def specialAppend(other: A, acc: CustomContainer[A]): CustomContainer[A] = acc.copy(data = other :: acc.data)
      }
    }
    
    object Foo {
      implicit class AppendableOps[A, C[_]](val c: C[A]) {
        def :+!(elem: A)(implicit appendable: Appendable[A, C]): C[A] = appendable.specialAppend(elem, c)
      }
    
      def main(args: Array[String]): Unit = {
        println(List(1,2,3,4) :+! 6)
        println(CustomContainer(List(1)) :+! 2)
        ()
      }
    }
    

    Yields:

    List(6, 1, 2, 3, 4)
    CustomContainer(List(2, 1))