1

I have the following family of functions (two of which are shown) that I would like to implement as a single generic function:

case class RichSeqInt(seq: Seq[Int]) extends AnyVal {
  def toByteArray: Array[Byte] = {
    val buffer = ByteBuffer.allocate(4 * seq.length)
    buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
    seq.foreach(buffer.putInt)
    buffer.array()
  }
}

case class RichSeqLong(seq: Seq[Long]) extends AnyVal {
  def toByteArray: Array[Byte] = {
    val buffer = ByteBuffer.allocate(8 * seq.length)
    buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
    seq.foreach(buffer.putLong)
    buffer.array()
  }
}

I can't figure out how to generically get the primitive object's size and then how to generically put it into the buffer even though this seems like something that should be doable.

Is this possible?

RandomBits
  • 4,194
  • 1
  • 17
  • 30
  • 1
    One consideration: watch out for calls to `.lenght` on `Seq`s: the abstraction is a bit too generic and the complexity can be linear for that (as in the case for `List`s, which are `Seq`s _default_ implementation -- using a factory method). – stefanobaghino Mar 17 '20 at 20:33

2 Answers2

3

You could use a type class but you'd lose the possibility of having a type value because this pattern requires the use of an implicit parameter:

import java.nio.{ByteBuffer, ByteOrder}

trait Bufferable[A] {
  def size: Int
  def put(a: A, buffer: ByteBuffer): Unit
}

implicit val intBufferable: Bufferable[Int] =
  new Bufferable[Int] {
    override val size = java.lang.Integer.SIZE / 8
    def put(n: Int, buffer: ByteBuffer): Unit = buffer.putInt(n)
  }

implicit val longBufferable: Bufferable[Long] =
  new Bufferable[Long] {
    override val size = java.lang.Long.SIZE / 8
    def put(n: Long, buffer: ByteBuffer): Unit = buffer.putLong(n)
  }

final case class RichSeq[A](seq: Seq[A])(implicit buf: Bufferable[A]) {
  def toByteArray: Array[Byte] = {
    val buffer = ByteBuffer.allocate(buf.size * seq.length)
    buffer.order(ByteOrder.LITTLE_ENDIAN)
    seq.foreach(buf.put(_, buffer))
    buffer.array()
  }
}

RichSeq(Vector(1, 2, 3)).toByteArray.size // evaluates to 12
RichSeq(Vector(1L, 2L, 3L)).toByteArray.size // evaluates to 24

You can play with this code here on Scastie.

If you can, you may probably want to consider the possibility of having a simple helper for this, so that you can avoid unnecessary allocations:

object SeqUtils {
  def toByteArray[A](seq: Seq[A])(implicit buf: Bufferable[A]): Array[Byte] = {
    val buffer = ByteBuffer.allocate(buf.size * seq.length)
    buffer.order(ByteOrder.LITTLE_ENDIAN)
    seq.foreach(buf.put(_, buffer))
    buffer.array()
  }
}

SeqUtils.toByteArray(Vector(1, 2, 3)).size
SeqUtils.toByteArray(Vector(1L, 2L, 3L)).size

The revised example is further available on Scastie.

If you want to know more about type classes there's plenty of material available online, I usually recommend this.

stefanobaghino
  • 11,253
  • 4
  • 35
  • 63
-2

This should work though I'm not sure I'd do it this way:

  def toByteArray[A: Manifest](seq: Seq[A]): Array[Byte] = seq match {
    case i: Seq[Int] if manifest <:< manifest[Int] => {
      val buffer = ByteBuffer.allocate(4 * seq.length)
      buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
      seq.foreach{x => buffer.putInt(x.asInstanceOf[Int])}
      buffer.array()
    }
    case l: Seq[Long] if manifest <:< manifest[Long] => {
      val buffer = ByteBuffer.allocate(8 * seq.length)
      buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
      seq.foreach{x => buffer.putLong(x.asInstanceOf[Long])}
      buffer.array()    
    }
    case _ => throw new java.lang.UnsupportedOperationException
  }

Of course this could be writter in a much cleaner way and certain common parts could be extracted. Also a reminder that A <:< B means A must be a subtype of B. It looks like a trick but It can come handy (I learned this years go here https://stackoverflow.com/a/3213914/497398).

mfirry
  • 3,634
  • 1
  • 26
  • 36
  • 2
    `case i: Seq[Int]` doesn't work, it is the same as `case i: Seq[_]`, it only works for the `if`. the manifest is kind of hacky and uncommon _(usually **ClassTag** is used instead)_ and this doesn't reduce the boilerplate at all. – Luis Miguel Mejía Suárez Mar 17 '20 at 21:34
  • `case i: Seq[Int] if manifest <:< manifest[Int] =>` definitely works. – mfirry Mar 18 '20 at 10:50
  • I agree it's a bit hacky, but it's a solution nonetheless. `ClassTag` is another way, I agree. – mfirry Mar 18 '20 at 10:51
  • Yes it works for the if, but it will produce erasure warnings, that is equivalent to `case i: Seq[_] if manifest <:< manifest[Int]` and at that point you are not pattern matching, so maybe an `if else` would be better. Also, again, the main problem is that the code is filled with casts that make everything hacky and that it isn't removing any boilerplate _(it is even adding more)_. – Luis Miguel Mejía Suárez Mar 18 '20 at 12:10