Especialización de funciones genéricas en Scala(o Java).
generics specialized-annotation (4)
¿Es posible especializar funciones genéricas (o clase) en Scala? Por ejemplo, quiero escribir una función genérica que escribe datos en un ByteBuffer:
def writeData[T](buffer: ByteBuffer, data: T) = buffer.put(data)
Pero como el método put toma solo un byte y lo pone en el búfer, debo especializarlo para Ints y Longs de la siguiente manera:
def writeData[Int](buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData[Long](buffer: ByteBuffer, data: Long) = buffer.putLong(data)
y no compilará. Por supuesto, en vez de eso, podría escribir 3 funciones diferentes writeByte, writeInt y writeLong respectivamente, pero digamos que hay otra función para una matriz:
def writeArray[T](buffer: ByteBuffer, array: Array[T]) {
for (elem <- array) writeData(buffer, elem)
}
y esto no funcionaría sin las funciones especializadas de escritura de datos: tendré que implementar otro conjunto de funciones writeByteArray, writeIntArray, writeLongArray. Tener que lidiar con la situación de esta manera cada vez que necesito usar funciones de escritura dependientes del tipo no es bueno. Hice algunas investigaciones y una posible solución es probar el tipo de parámetro:
def writeArray[T](buffer: ByteBuffer, array: Array[T]) {
if (array.isInstanceOf[Array[Byte]])
for (elem <- array) writeByte(buffer, elem)
else if (array.isInstanceOf[Array[Int]])
for (elem <- array) writeInt(buffer, elem)
...
}
Esto podría funcionar pero es menos eficiente porque la comprobación de tipos se realiza en tiempo de ejecución, a diferencia de la versión de función especializada.
Entonces, mi pregunta es, ¿cuál es la forma más deseable y preferida de resolver este tipo de problema en Scala o Java? ¡Agradezco tu ayuda de antemano!
Las declaraciones
def writeData[Int](buffer: ByteBuffer, data: Int) def writeData[Long](buffer: ByteBuffer, data: Long)
no compile porque son equivalentes, ya que Int y Long son parámetros de tipo formal
y no los tipos estándar de Scala. Para definir funciones con tipos estándar de Scala solo escribe:
def writeData(buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData(buffer: ByteBuffer, data: Long) = buffer.putLong(data)
De esta manera declaras diferentes funciones con el mismo nombre.
Como son funciones diferentes, no puede aplicarlas a elementos de una lista de tipo estáticamente desconocido. Primero tienes que determinar el tipo de la Lista. Tenga en cuenta que puede suceder que el tipo de la Lista sea AnyRef, entonces usted determinó dinámicamente el tipo de cada elemento. La determinación se puede hacer con
isInstanceOf
como en su código original, o con la coincidencia de patrones, como sugiererolve
. Creo que esto produciría el mismo bytecode.En suma, tienes que elegir:
código rápido con múltiples funciones como
writeByteArray, writeIntArray
etc. Todos pueden tener el mismo nombre dewriteArray
pero pueden distinguirse estáticamente por sus parámetros reales. La variante sugerida por Dominic Bou-Sa es de este tipo.Código conciso pero lento con determinación de tipo en tiempo de ejecución
Desafortunadamente, no puedes tener un código rápido y conciso.
¿No sería bueno si pudieras tener una solución compacta y eficiente? Resulta que puedes @specialized
, dada la función @specialized
de Scala. Primero, una advertencia: la función es un poco defectuosa y puede fallar si intentas usarla para algo demasiado complicado. Pero para este caso, es casi perfecto.
La anotación @specialized
crea clases y / o métodos separados para cada tipo primitivo, y luego llama a eso en lugar de a la versión genérica cada vez que el compilador sabe con seguridad cuál es el tipo primitivo. El único inconveniente es que hace todo esto de forma completamente automática: no puedes completar tu propio método. Eso es una vergüenza, pero puedes superar el problema usando clases de tipos.
Veamos algunos códigos:
import java.nio.ByteBuffer
trait BufferWriter[@specialized(Byte,Int) A]{
def write(b: ByteBuffer, a: A): Unit
}
class ByteWriter extends BufferWriter[Byte] {
def write(b: ByteBuffer, a: Byte) { b.put(a) }
}
class IntWriter extends BufferWriter[Int] {
def write(b: ByteBuffer, a: Int) { b.putInt(a) }
}
object BufferWriters {
implicit val byteWriter = new ByteWriter
implicit val intWriter = new IntWriter
}
Esto nos da un rasgo BufferWriter
que es genérico, pero reemplazamos cada uno de los tipos primitivos específicos que queremos (en este caso, Byte
e Int
) con una implementación apropiada. La especialización es lo suficientemente inteligente como para vincular esta versión explícita con la oculta que normalmente utiliza para la especialización. Así que tienes tu código personalizado, pero ¿cómo lo usas? Aquí es donde entran los valores implícitos (lo he hecho de esta manera por velocidad y claridad):
import BufferWriters._
def write[@specialized(Byte,Int) A: BufferWriter](b: ByteBuffer, ar: Array[A]) {
val writer = implicitly[BufferWriter[A]]
var i = 0
while (i < ar.length) {
writer.write(b, ar(i))
i += 1
}
}
La notación A: BufferWriter
significa que para llamar a este método de write
, debe tener a mano un BufferWriter[A]
implícito BufferWriter[A]
. Los hemos suministrado con los BufferWriters
en BufferWriters
, por lo que debemos establecerlos. Vamos a ver si esto funciona.
val b = ByteBuffer.allocate(6)
write(b, Array[Byte](1,2))
write(b, Array[Int](0x03040506))
scala> b.array
res3: Array[Byte] = Array(1, 2, 3, 4, 5, 6)
Si pones estas cosas en un archivo y empiezas a buscar en las clases con javap -c -private
, verás que se están utilizando los métodos primitivos apropiados.
(Tenga en cuenta que si no usara la especialización, esta estrategia aún funcionaría, pero tendría que incluir valores dentro del bucle para copiar la matriz).
Qué tal esto:
def writeData(buffer: ByteBuffer, data: AnyVal) {
data match {
case d: Byte => buffer put d
case d: Int => buffer putInt d
case d: Long => buffer putLong d
...
}
}
Aquí, hace la distinción entre mayúsculas y minúsculas en el método writeData
, lo que hace que todos los métodos posteriores sean muy simples:
def writeArray(buffer: ByteBuffer, array: Array[AnyVal]) {
for (elem <- array) writeData(buffer, elem)
}
Ventajas: Simple, corto, fácil de entender.
Desventajas: No es completamente seguro si no maneja todos los tipos de AnyVal
: alguien puede llamar writeData(buffer, ())
(el segundo argumento es de tipo Unit
), lo que puede resultar en un error en el tiempo de ejecución. Pero también puede hacer que el manejo de ()
un no-op, que resuelve el problema. El método completo se vería así:
def writeData(buffer: ByteBuffer, data: AnyVal) {
data match {
case d: Byte => buffer put d
case d: Short => buffer putShort d
case d: Int => buffer putInt d
case d: Long => buffer putLong d
case d: Float => buffer putFloat d
case d: Double => buffer putDouble d
case d: Char => buffer putChar d
case true => buffer put 1.asInstanceOf[Byte]
case false => buffer put 0.asInstanceOf[Byte]
case () =>
}
}
Por cierto, esto solo funciona tan fácilmente debido a la naturaleza estricta orientada a objetos de Scala. En Java, donde los tipos primitivos no son objetos, esto sería mucho más engorroso. Ahí, realmente tendrías que crear un método separado para cada tipo primitivo, a menos que quieras hacer un poco de boxeo feo y unboxing.
Utilice un patrón de typeclass. Tiene la ventaja sobre la comprobación de ejemplo (o la coincidencia de patrones) de la seguridad de tipos.
import java.nio.ByteBuffer
trait BufferWriter[A] {
def write(buffer: ByteBuffer, a: A)
}
class BuffPimp(buffer: ByteBuffer) {
def writeData[A: BufferWriter](data: A) = {
implicitly[BufferWriter[A]].write(buffer, data)
}
}
object BuffPimp {
implicit def intWriter = new BufferWriter[Int] {
def write(buffer: ByteBuffer, a: Int) = buffer.putInt(a)
}
implicit def doubleWriter = new BufferWriter[Double] {
def write(buffer: ByteBuffer, a: Double) = buffer.putDouble(a)
}
implicit def longWriter = new BufferWriter[Long] {
def write(buffer: ByteBuffer, a: Long) = buffer.putLong(a)
}
implicit def wrap(buffer: ByteBuffer) = new BuffPimp(buffer)
}
object Test {
import BuffPimp._
val someByteBuffer: ByteBuffer
someByteBuffer.writeData(1)
someByteBuffer.writeData(1.0)
someByteBuffer.writeData(1L)
}
Así que este código no es la mejor demostración de typeclasses. Todavía soy muy nuevo para ellos. Este video brinda una descripción realmente sólida de sus beneficios y cómo puede usarlos: http://www.youtube.com/watch?v=sVMES4RZF-8