scalacollectionsshapelessheterogeneous

How to implement an heterogeneous container in Scala


I need an heterogeneous, typesafe container to store unrelated type A, B, C.

Here is a kind of type-level specification :

trait Container {
  putA(a: A) 
  putB(b: B)
  putC(c: C)
  put(o: Any) = { o match {
    case a: A => putA(a)
    case b: B => putB(b)
    case c: C => putC(c)
  }
  getAllAs : Seq[A]
  getAllBs : Seq[B]
  getAllCs : Seq[C]
}

Which type is best suites to backed this container ?

Is it worth creating a Containerable[T] typeclass for types A, B, C ?

thks.


Solution

  • As other have suggested, you can leverage shapeless' Coproduct type. Here's an example.

    // let's define a Coproduct of the two types you want to support
    type IS = Int :+: String :+: CNil
    
    // now let's have a few instances
    val i = Coproduct[IS](42)
    val i2 = Coproduct[IS](43)
    val s = Coproduct[IS]("foo")
    val s2 = Coproduct[IS]("bar")
    
    // let's put them in a container
    val cont = List(i, s, i2, s2)
    
    // now, do you want all the ints?
    val ints = cont.map(_.select[Int]).flatten
    
    // or all the strings?
    val strings = cont.map(_.select[String]).flatten
    
    // and of course you can add elements (it's a List)
    val cont2 = Coproduct[IS](12) :: cont
    val cont3 = Coproduct[IS]("baz") :: cont2
    

    Now this is of course not the most intuitive API for a generic container, but can easily encapsulate the logic inside a custom class using a Coproduct for representing the multiple types.

    Here's a sketch of an implementation

    import shapeless._; import ops.coproduct._
    
    class Container[T <: Coproduct] private (underlying: List[T]) {
      def ::[A](a: A)(implicit ev: Inject[T, A]) =
        new Container(Coproduct[T](a) :: underlying)
    
      def get[A](implicit ev: Selector[T, A]) =
        underlying.map(_.select[A]).flatten
    
      override def toString = underlying.toString
    }
    
    object Container {
      def empty[T <: Coproduct] = new Container(List[T]())
    }
    

    Example

    scala> type IS = Int :+: String :+: CNil
    defined type alias IS
    
    scala> val cont = 42 :: "foo" :: "bar" :: 43 :: Container.empty[IS]
    cont: Container[IS] = List(42, foo, bar, 43)
    
    scala> cont.get[Int]
    res0: List[Int] = List(42, 43)
    
    scala> cont.get[String]
    res1: List[String] = List(foo, bar)