Пока это моя лучшая попытка. Это работает, но реализация не очень (даже если результаты). Улучшения приветствуются!
Существует способ сделать это без макросов, как на уровне класса, так и на уровне метода, и он включает классы типов — их довольно много! И ответ не совсем одинаков для классов и методов. Так что терпите меня.
Специализированные классы вручную
Вы вручную специфицируете классы так же, как вы вручную предоставляете любую различную реализацию для классов: ваш суперкласс является абстрактным (или является трейтом), а подклассы предоставляют детали реализации.
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 }
}
Теперь, если бы у нас был специальный метод для выбора правильных реализаций, мы бы сделали:
object Bippy{
def apply[@specialized(Int) B](initial: B) = ??? // Now what?
}
Таким образом, мы преобразовали нашу проблему предоставления пользовательских специализированных классов и в необходимость предоставления пользовательских специализированных методов.
Ручные специализированные методы
Ручная специализация метода требует способа написать одну реализацию, которая, тем не менее, может выбирать, какую реализацию вы хотите (во время компиляции). Типовые классы отлично справляются с этим. Предположим, у нас уже есть классы типов, которые реализуют всю нашу функциональность, и что компилятор выберет правильный. Тогда мы могли бы просто написать
def foo[@specialized(Int) A: SpecializedFooImpl](a: A): String =
implicitly[SpecializedFooImpl[A]](a)
... или мы могли бы, если бы implicitly
гарантированно сохраняло специализацию и если бы нам когда-либо был нужен только один параметр типа. В общем случае это неверно, поэтому мы напишем наш класс типов как неявный параметр, а не полагаемся на синтаксический сахар A: TC
.
def foo[@specialized(Int) A](a: A)(implicit impl: SpecializedFooImpl[A]): String =
impl(a)
(На самом деле, это в любом случае меньше шаблонов.)
Таким образом, мы преобразовали нашу проблему предоставления пользовательских специализированных методов в необходимость написания специализированных классов типов и получения компилятором заполнения правильных.
Специализированные вручную классы типов
Классы типов — это просто классы, и теперь нам снова нужно писать специализированные классы, но есть критическая разница. Пользователь не запрашивает произвольные экземпляры. Это дает нам достаточно дополнительной гибкости для работы.
Для foo
нам нужна версия Int
и полностью универсальная версия.
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)
}
Теперь мы могли бы создавать имплициты для предоставления этих классов типов, например так:
implicit def specFooAsAny[A] = new SpecFooImplAny[A]
implicit val specFooAsInt = new SpecFooImplInt
за исключением того, что у нас есть проблема: если мы на самом деле попытаемся вызвать foo: Int
, будут применены оба имплицита. Так что, если бы у нас был способ расставить приоритеты, какой класс типов мы выбрали, все было бы готово.
Выбор классов типов (и имплицитов в целом)
Один из секретных ингредиентов, который компилятор использует для определения правильного неявного использования, — это наследование. Если имплициты исходят от A
через B extends A
, но B
объявляет свои собственные, которые также могут применяться, те, что в B
, выигрывают, если все остальные равны. Поэтому мы помещаем тех, кого хотим получить, глубже в иерархию наследования.
Кроме того, поскольку вы можете свободно определять имплициты в трейтах, вы можете смешивать их где угодно.
Итак, последняя часть нашей головоломки — вставить имплициты нашего класса типов в цепочку свойств, которые расширяют друг друга, причем более общие свойства появляются раньше.
trait LowPriorityFooSpecializers {
implicit def specializeFooAsAny[A] = new SpecializedFooImplAny[A]
}
trait FooSpecializers extends LowPriorityFooSpecializers {
implicit val specializeFooAsInt = new SpecializedFooImplInt
}
Смешайте трейт с наивысшим приоритетом везде, где необходимы имплициты, и классы типов будут выбраны по желанию.
Обратите внимание, что классы типов будут настолько специализированными, насколько вы их сделаете, даже если специализированная аннотация не используется. Таким образом, вы можете вообще обойтись без specialized
, если вы достаточно точно знаете тип, если только вы не хотите использовать специализированные функции или взаимодействовать с другими специализированными классами. (И вы, вероятно, знаете.)
Полный пример
Предположим, мы хотим создать специализированную функцию bippy
с двумя параметрами, которая будет применять следующее преобразование:
bippy(a, b) -> b
bippy(a, b: Int) -> b+1
bippy(a: Int, b) -> b
bippy(a: Int, b: Int) -> a+b
Мы должны добиться этого с помощью трех классов типов и одного специализированного метода. Попробуем сначала метод:
def bippy[@specialized(Int) A, @specialized(Int) B](a: A, b: B)(implicit impl: SpecBippy[A, B]) =
impl(a, b)
Затем классы типов:
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
}
Затем имплициты в связанных трейтах:
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
}
и, наконец, мы попробуем это (после того, как соединим все вместе в :paste
в 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
Это работает — наши пользовательские реализации включены. Просто чтобы убедиться, что мы можем использовать любой тип, но не попадаем в неправильную реализацию:
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
И, наконец, чтобы убедиться, что мы на самом деле избежали упаковки, мы посчитаем время bippy
при суммировании группы целых чисел:
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
Таким образом, мы видим, что наш специализированный bippy-adder достигает той же производительности, что и специализированная функция Function2 (около 3 операций добавления за нс, что достаточно для современной машины).
Резюме
Чтобы написать собственный специализированный код с использованием аннотации @specialized
,
- Сделайте специализированный класс абстрактным и вручную предоставьте конкретные реализации
- Сделать так, чтобы специализированные методы (включая генераторы для специализированного класса) принимали классы типов, выполняющие реальную работу.
- Сделайте черту базового класса типов
@specialized
и предоставьте конкретные реализации
- Предоставьте неявные vals или defs в иерархии наследования признаков, чтобы выбрать правильный.
Это много шаблонов, но в конце концов вы получаете безупречный специализированный опыт.
person
Rex Kerr
schedule
06.04.2015