Luciano
Luciano

Reputation: 2418

How can I extend Scala collections with member values?

Say I have the following data structure:

case class Timestamped[CC[M] < Seq[M]](elems : CC, timestamp : String)

So it's essentially a sequence with an attribute -- a timestamp -- attached to it. This works fine and I could create new instances with the syntax

val t = Timestamped(Seq(1,2,3,4),"2014-02-25")
t.elems.head // 1
t.timestamp  // "2014-05-25"

The syntax is unwieldly and instead I want to be able to do something like:

Timestamped(1,2,3,4)("2014-02-25")
t.head      // 1
t.timestamp // "2014-05-25"

Where timestamped is just an extension of a Seq and it's implementation SeqLike, with a single attribute val timestamp : String.

This seems easy to do; just use a Seq with a mixin TimestampMixin { val timestamp : String }. But I can't figure out how to create the constructor. My question is: how do I create a constructor in the companion object, that creates a sequence with an extra member value? The signature is as follows:

object Timestamped {
  def apply(elems: M*)(timestamp : String) : Seq[M] with TimestampMixin = ???
}

You'll see that it's not straightforward; collections use Builders to instantiate themselves, so I can't simply call the constructor an override some vals.

Upvotes: 0

Views: 187

Answers (2)

Rich Henry
Rich Henry

Reputation: 1849

Go the other way... extend Seq, it only has 3 abstract members:

  case class Stamped[T](elems: Seq[T], stamp: Long) extends Seq[T] {
    override def apply(i: Int) = elems.apply(i)
    override def iterator = elems.iterator
    override def length = elems.length
  }

  val x = Stamped(List(10,20,30), 15L)

  println(x.head)              // 10
  println(x.timeStamp)         // 15
  println(x.map { _ * 10})     // List(100, 200, 300)
  println(x.filter { _ > 20})  // List(30)

Keep in mind, this only works as long as Seq is specific enough for your use cases, if you later find you need more complex collection behavior this may become untenable.

EDIT: Added a version closer to the signature you were trying to create. Not sure if this helps you any more:

  case class Stamped[T](elems: T*)(stamp: Long) extends Seq[T] {
    def timeStamp = stamp
    override def apply(i: Int) = elems.apply(i)
    override def iterator = elems.iterator
    override def length = elems.length
  }  

  val x = Stamped(10,20,30)(15L)

  println(x.head)              // 10
  println(x.timeStamp)         // 15
  println(x.map { _ * 10})     // List(100, 200, 300)
  println(x.filter { _ > 20})  // List(30)

Where elems would end up being a generically created WrappedArray.

Upvotes: 0

Michael Zajac
Michael Zajac

Reputation: 55569

Scala collections are very complicated structures when it comes down to it. Extending Seq requires implementing apply, length, and iterator methods. In the end, you'll probably end up duplicating existing code for List, Set, or something else. You'll also probably have to worry about CanBuildFroms for your collection, which in the end I don't think is worth it if you just want to add a field.

Instead, consider an implicit conversion from your Timestamped type to Seq.

case class Timestamped[A](elems: Seq[A])(timestamp: String)

object Timestamped {
    implicit def toSeq[A](ts: Timestamped[A]): Seq[A] = ts.elems
}

Now, whenever I try to call a method from Seq, the compiler will implicitly convert Timestamped to Seq, and we can proceed as normal.

scala> val ts = Timestamped(List(1,2,3,4))("1/2/34")
ts: Timestamped[Int] = Timestamped(List(1, 2, 3, 4))

scala> ts.filter(_ > 2)
res18: Seq[Int] = List(3, 4)

There is one major drawback here, and it's that we're now stuck with Seq after performing operations on the original Timestamped.

Upvotes: 2

Related Questions