We can copy an inputstream to an outputstream in a generic and type-safe manner using typeclasses. A typeclass is a concept. It's one approach to polymorphism. In particular, it's parametric polymorphism because the polymorphic behavior is encoded using parameters. In our case, our parameters will be generic types to Scala traits.
Let's make Reader[I]
and Writer[O]
traits, where I
and O
are input and output stream types, respectively.
trait Reader[I] {
def read(input: I, buffer: Array[Byte]): Int
}
trait Writer[O] {
def write(output: O, buffer: Array[Byte], startAt: Int, nBytesToWrite: Int): Unit
}
We can now make a generic copy method that can operate on things that subscribe to these interfaces.
object CopyStreams {
type Bytes = Int
def apply[I, O](input: I, output: O, chunkSize: Bytes = 1024)(implicit r: Reader[I], w: Writer[O]): Unit = {
val buffer = Array.ofDim[Byte](chunkSize)
var count = -1
while ({count = r.read(input, buffer); count > 0})
w.write(output, buffer, 0, count)
}
}
Note the implicit r
and w
parameters here. Essentially, we're saying that CopyStreams[I,O].apply
will work iff there are Reader[I]
and a Writer[O]
values in scope. This will make us able to call CopyStreams(input, output) seamlessly.
Importantly, however, note that this implementation is generic. It operates on types that are independent of actual stream implementations.
In my particular use case, I needed to copy S3 objects to local files. So I made the following implicit values.
object Reader {
implicit val s3ObjectISReader = new Reader[S3ObjectInputStream] {
@inline override def read(input: S3ObjectInputStream, buffer: Array[Byte]): Int =
input.read(buffer)
}
}
object Writer {
implicit val fileOSWriter = new Writer[FileOutputStream] {
@inline override def write(output: FileOutputStream,
buffer: Array[Byte],
startAt: Int,
nBytesToWrite: Int): Unit =
output.write(buffer, startAt, nBytesToWrite)
}
}
So now I can do the following:
val input:S3ObjectStream = ...
val output = new FileOutputStream(new File(...))
import Reader._
import Writer._
CopyStreams(input, output)
// close and such...
And if we ever need to copy different stream types, we only need to write a new Reader
or Writer
implicit value. We can use the CopyStreams
code without changing it!
BufferedInputStream
there. – Premises