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?

1
  • 1
    One consideration: watch out for calls to .lenght on Seqs: the abstraction is a bit too generic and the complexity can be linear for that (as in the case for Lists, which are Seqs default implementation -- using a factory method). Commented Mar 17, 2020 at 20:33

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Move the (implicit buf: Bufferable[A] to the method and you recover your anyval. Also, it would be better to move those implicits to the companion object of the typeclass.
Thanks. I have it working, but where should I place the implicit's so that they are automatically found? I tried both in SeqUtils and in RichSeq with no luck. I am currently importing them explicitly.
@RandomBits Move them to the companion object of Bufferable.
Duh, of course, it makes perfect sense for them to live there.
And please don't use the name Bufferable in your project, I'm sure you can find a better name. :D
-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).

4 Comments

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.
case i: Seq[Int] if manifest <:< manifest[Int] => definitely works.
I agree it's a bit hacky, but it's a solution nonetheless. ClassTag is another way, I agree.
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).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.