¿Cómo se pueden proporcionar implementaciones especializadas manualmente con la especialización de Scala?
specialization (3)
La especialización promete proporcionar implementaciones de alta eficiencia para tipos primitivos con un mínimo de placa de caldera adicional. Pero la especialización parece estar demasiado ansiosa por su propio bien. Si quiero especializar una clase o método,
def foo[@specialized(Byte) A](a: A): String = ???
class Bar[@specialized(Int) B] {
var b: B = ???
def baz: B = ???
}
luego debo escribir una implementación única que cubra tanto los casos especializados como los genéricos. ¿Qué sucede si esos casos son realmente diferentes entre sí, de modo que las implementaciones no se superponen? Por ejemplo, si quisiera realizar operaciones matemáticas en bytes, necesitaría insertar un montón de & 0xFF
s en la lógica.
Posiblemente podría escribir una clase de tipos especializada para hacer las matemáticas correctamente, pero ¿no es eso lo que hace retroceder el mismo problema un nivel? ¿Cómo escribo mi método especializado +
para esa clase de tipos de manera que no entre en conflicto con una implementación más general?
class Adder[@specialized(Byte) A] {
def +(a1: A, a2: A): A = ???
}
Además, una vez que creo una clase de tipos de esta manera, ¿cómo me aseguro de que se utilice la clase de tipos correcta para mis métodos especializados en lugar de la versión general (que, si es realmente general, probablemente debería compilarse y ciertamente se ejecutaría? excepto que no es lo que quiero)?
¿Hay una manera de hacer esto sin macros? ¿Es más fácil con macros?
Esta es una respuesta de la lista de correo interno de Scala :
Con la especialización de miniboxing , puede utilizar la función de reflexión:
import MbReflection._
import MbReflection.SimpleType._
import MbReflection.SimpleConv._
object Test {
def bippy[@miniboxed A, @miniboxed B](a: A, b: B): B =
(reifiedType[A], reifiedType[B]) match {
case (`int`, `int`) => (a.as[Int] + b.as[Int]).as[B]
case ( _ , `int`) => (b.as[Int] + 1).as[B]
case (`int`, _ ) => b
case ( _ , _ ) => b
}
def main(args: Array[String]): Unit = {
def x = 1.0
assert(bippy(3,4) == 7)
assert(bippy(x,4) == 5)
assert(bippy(3,x) == x)
assert(bippy(x,x) == x)
}
}
De esta manera, puede elegir el comportamiento exacto del método bippy
basado en los argumentos de tipo sin definir ninguna clase implícita.
Este es mi mejor intento hasta ahora. Funciona pero la implementación no es bonita (incluso si los resultados lo son). ¡Las mejoras son bienvenidas!
Hay una manera de hacer esto sin macros, tanto a nivel de clase como de método, e involucra clases de tipos, ¡muchas de ellas! Y la respuesta no es exactamente la misma para las clases y los métodos. Así que tengan paciencia conmigo.
Clases Manuales Especializadas
Usted especializa las clases manualmente de la misma manera que proporciona manualmente cualquier tipo de implementación diferente para las clases: su superclase es abstracta (o es un rasgo), y las subclases proporcionan los detalles de la implementación.
abstract class Bippy[@specialized(Int) B] {
def b: B
def next: Bippy[B]
}
class BippyInt(initial: Int) extends Bippy[Int] {
private var myB: Int = initial
def b: Int = myB
def next = { myB += 1; this }
}
class BippyObject(initial: Object) extends Bippy[Object] {
private var myB: Object = initial
def b: B = myB
def next = { myB = myB.toString; this }
}
Ahora, si solo tuviéramos un método especializado para elegir las implementaciones correctas, habríamos terminado:
object Bippy{
def apply[@specialized(Int) B](initial: B) = ??? // Now what?
}
Así que hemos convertido nuestro problema de proporcionar clases y métodos especializados personalizados en la necesidad de proporcionar métodos especializados personalizados.
Métodos Manualmente Especializados
La especialización manual de un método requiere una forma de escribir una implementación que, sin embargo, pueda seleccionar la implementación que desee (en tiempo de compilación). Las clases de tipos son geniales en esto. Supongamos que ya tenemos clases de tipos que implementaron todas nuestras funcionalidades, y que el compilador seleccionaría la correcta. Entonces podríamos simplemente escribir
def foo[@specialized(Int) A: SpecializedFooImpl](a: A): String =
implicitly[SpecializedFooImpl[A]](a)
... o podríamos si estaba garantizado implicitly
para preservar la especialización y si solo quisiéramos un solo tipo de parámetro. En general, estas cosas no son ciertas, por lo que escribiremos nuestra clase de tipos como un parámetro implícito en lugar de confiar en el azúcar sintáctico A: TC
.
def foo[@specialized(Int) A](a: A)(implicit impl: SpecializedFooImpl[A]): String =
impl(a)
(En realidad, eso es menos repetitivo de todos modos).
Así que hemos convertido nuestro problema de proporcionar métodos especializados personalizados en la necesidad de escribir clases de tipos especializadas y hacer que el compilador complete las correctas.
Clases de Tipo Especializadas Manualmente
Las clases de tipos son solo clases, y ahora tenemos que escribir clases especializadas de nuevo, pero hay una diferencia crítica. El usuario no es el que pide instancias arbitrarias. Esto nos da suficiente flexibilidad adicional para que funcione.
Para foo
, necesitamos una versión Int
y una versión totalmente genérica.
trait SpecFooImpl[@specialized (Int), A] {
def apply(param: A): String
}
final class SpecFooImplAny[A] extends SpecFooImpl[A] {
def apply(param: A) = param.toString
}
final class SpecFooImplInt extends SpecFooImpl[Int] {
def apply(param: Int) = "!" * math.max(0, param)
}
Ahora podríamos crear implícitos para suministrar esas clases de tipos como tales.
implicit def specFooAsAny[A] = new SpecFooImplAny[A]
implicit val specFooAsInt = new SpecFooImplInt
excepto que tenemos un problema: si realmente intentamos llamar a foo: Int
, se aplicarán ambas implicaciones. Entonces, si tuviéramos una manera de priorizar qué tipo de clase elegimos, estaríamos listos.
Selección de clases de tipo (e implícitas en general).
Uno de los ingredientes secretos que utiliza el compilador para determinar el derecho implícito de usar es la herencia. Si las implicaciones provienen de A
través de B extends A
, pero B
declara que también podrían aplicarse, los de B
ganan si todo lo demás es igual. Así que ponemos los que queremos ganar más en la jerarquía de herencia.
Además, dado que eres libre de definir implicaciones en los rasgos, puedes mezclarlos en cualquier lugar.
Así que la última pieza de nuestro rompecabezas es hacer estallar nuestras implicaciones de clase de tipo en una cadena de rasgos que se extienden entre sí, con los más genéricos que aparecen antes.
trait LowPriorityFooSpecializers {
implicit def specializeFooAsAny[A] = new SpecializedFooImplAny[A]
}
trait FooSpecializers extends LowPriorityFooSpecializers {
implicit val specializeFooAsInt = new SpecializedFooImplInt
}
Mezcle el rasgo de prioridad más alta donde sea que se necesiten las implicaciones, y las clases de tipo se seleccionarán como se desee.
Tenga en cuenta que las clases de tipos serán tan especializadas como las realice, incluso si no se utiliza la anotación especializada . Por lo tanto, puede prescindir de todo lo specialized
, siempre que conozca el tipo lo suficientemente preciso, a menos que desee utilizar funciones especializadas o interoperar con otras clases especializadas. (Y probablemente lo hagas).
Un ejemplo completo
Supongamos que queremos hacer una función bippy
especializada de dos parámetros que aplique la siguiente transformación:
bippy(a, b) -> b
bippy(a, b: Int) -> b+1
bippy(a: Int, b) -> b
bippy(a: Int, b: Int) -> a+b
Deberíamos poder lograr esto con tres clases de tipos y un solo método especializado. Probemos, primero el método:
def bippy[@specialized(Int) A, @specialized(Int) B](a: A, b: B)(implicit impl: SpecBippy[A, B]) =
impl(a, b)
Luego las clases de tipo:
trait SpecBippy[@specialized(Int) A, @specialized(Int) B] {
def apply(a: A, b: B): B
}
final class SpecBippyAny[A, B] extends SpecBippy[A, B] {
def apply(a: A, b: B) = b
}
final class SpecBippyAnyInt[A] extends SpecBippy[A, Int] {
def apply(a: A, b: Int) = b + 1
}
final class SpecBippyIntInt extends SpecBippy[Int, Int] {
def apply(a: Int, b: Int) = a + b
}
Entonces las implicaciones en rasgos encadenados:
trait LowerPriorityBippySpeccer {
// Trick to avoid allocation since generic case is erased anyway!
private val mySpecBippyAny = new SpecBippyAny[AnyRef, AnyRef]
implicit def specBippyAny[A, B] = mySpecBippyAny.asInstanceOf[SpecBippyAny[A, B]]
}
trait LowPriorityBippySpeccer extends LowerPriorityBippySpeccer {
private val mySpecBippyAnyInt = new SpecBippyAnyInt[AnyRef]
implicit def specBippyAnyInt[A] = mySpecBippyAnyInt.asInstanceOf[SpecBippyAnyInt[A]]
}
// Make this last one an object so we can import the contents
object BippySpeccer extends LowPriorityBippySpeccer {
implicit val specBippyIntInt = new SpecBippyIntInt
}
y finalmente lo probaremos (después de pegar todo en :paste
en el REPL):
scala> import Speccer._
import Speccer._
scala> bippy(Some(true), "cod")
res0: String = cod
scala> bippy(1, "salmon")
res1: String = salmon
scala> bippy(None, 3)
res2: Int = 4
scala> bippy(4, 5)
res3: Int = 9
Funciona - nuestras implementaciones personalizadas están habilitadas. Solo para verificar que podamos usar cualquier tipo, pero no nos filtramos en la implementación incorrecta:
scala> bippy(4, 5: Short)
res4: Short = 5
scala> bippy(4, 5: Double)
res5: Double = 5.0
scala> bippy(3: Byte, 2)
res6: Int = 3
Y, finalmente, para verificar que realmente hayamos evitado el boxeo, bippy
un montón de enteros:
scala> val th = new ichi.bench.Thyme
th: ichi.bench.Thyme = ichi.bench.Thyme@1130520d
scala> val adder = (i: Int, j: Int) => i + j
adder: (Int, Int) => Int = <function2>
scala> var a = Array.fill(1024)(util.Random.nextInt)
a: Array[Int] = Array(-698116967, 2090538085, -266092213, ...
scala> th.pbenchOff(){
var i, s = 0
while (i < 1024) { s = adder(a(i), s); i += 1 }
s
}{
var i, s = 0
while (i < 1024) { s = bippy(a(i), s); i += 1 }
s
}
Benchmark comparison (in 1.026 s)
Not significantly different (p ~= 0.2795)
Time ratio: 0.99424 95% CI 0.98375 - 1.00473 (n=30)
First 330.7 ns 95% CI 328.2 ns - 333.1 ns
Second 328.8 ns 95% CI 326.3 ns - 331.2 ns
Así que podemos ver que nuestro bippy-adder especializado logra el mismo tipo de rendimiento que Function2 especializado (aproximadamente 3 adiciones por ns, lo que es adecuado para una máquina moderna).
Resumen
Para escribir código especializado personalizado utilizando la anotación @specialized
,
- Hacer la clase especializada abstracta y suministrar manualmente implementaciones concretas.
- Los métodos especializados (incluidos los generadores para una clase especializada) toman clases de texto que hacen el trabajo real
- Realice el rasgo de
@specialized
tipos de base@specialized
y proporcione implementaciones concretas - Proporcione valores o defs implícitos en una jerarquía de herencia de rasgos para que se seleccione el correcto
Es un montón de repetitivo, pero al final de todo, obtienes una experiencia personalizada personalizada perfecta.
Sé que es bastante viejo, pero lo encontré buscando otra cosa y quizás resulte útil. Tenía una motivación similar y la respondí en cómo verificar si estoy dentro de una función o clase especializada
Utilicé una tabla de búsqueda inversa: SpecializedKey
es una clase especializada que es igual a todas las demás instancias con la misma especialización, por lo que puedo realizar una verificación como esta
def onlyBytes[@specialized E](arg :E) :Option[E] =
if (specializationFor[E]==specializationFor[Byte]) Some(arg)
else None
Por supuesto, no hay beneficios de rendimiento cuando se trabaja con valores primitivos individuales, pero con colecciones, especialmente iteradores, se vuelve útil.
final val AllButUnit = new Specializable.Group((Byte, Short, Int, Long, Char, Float, Double, Boolean, AnyRef))
def specializationFor[@specialized(AllButUnit) E] :ResolvedSpecialization[E] =
Specializations(new SpecializedKey[E]).asInstanceOf[ResolvedSpecialization[E]]
private val Specializations = Seq(
resolve[Byte],
resolve[Short],
resolve[Int],
resolve[Long],
resolve[Char],
resolve[Float],
resolve[Double],
resolve[Boolean],
resolve[Unit],
resolve[AnyRef]
).map(
spec => spec.key -> spec :(SpecializedKey[_], ResolvedSpecialization[_])
).toMap.withDefaultValue(resolve[AnyRef])
private def resolve[@specialized(AllButUnit) E :ClassTag] :ResolvedSpecialization[E] =
new ResolvedSpecialization[E](new SpecializedKey[E], new Array[E](0))
class ResolvedSpecialization[@specialized(AllButUnit) E] private[SpecializedCompanion]
(val array :Array[E], val elementType :Class[E], val classTag :ClassTag[E], private[SpecializedCompanion] val key :SpecializedKey[E]) {
private[SpecializedCompanion] def this(key :SpecializedKey[E], array :Array[E]) =
this(array, array.getClass.getComponentType.asInstanceOf[Class[E]], ClassTag(array.getClass.getComponentType.asInstanceOf[Class[E]]), key)
override def toString = s"@specialized($elementType)"
override def equals(that :Any) = that match {
case r :ResolvedSpecialization[_] => r.elementType==elementType
case _ => false
}
override def hashCode = elementType.hashCode
}
private class SpecializedKey[@specialized(AllButUnit) E] {
override def equals(that :Any) = that.getClass==getClass
override def hashCode = getClass.hashCode
def className = getClass.getName
override def toString = className.substring(className.indexOf("$")+1)
}