Как можно предоставить специализированные реализации со специализацией Scala?

Специализация обещает предоставить высокоэффективные реализации для примитивных типов с минимальным дополнительным шаблоном. Но специализация, кажется, слишком стремится к своей же пользе. Если я хочу специализировать класс или метод,

def foo[@specialized(Byte) A](a: A): String = ???

class Bar[@specialized(Int) B] {
  var b: B = ???
  def baz: B = ???
}

затем мне нужно написать единую реализацию, которая охватывает как специализированные, так и общие случаи. Что, если эти случаи действительно отличаются друг от друга, так что реализации не пересекаются? Например, если бы я хотел выполнить математические операции с байтами, мне нужно было бы вставить в логику кучу & 0xFF.

Возможно, я мог бы написать специализированный класс типов для правильного расчета, но разве это не отодвигает ту же проблему на один уровень назад? Как мне написать свой специализированный метод + для этого класса типов таким образом, чтобы он не конфликтовал с более общей реализацией?

class Adder[@specialized(Byte) A] {
  def +(a1: A, a2: A): A = ???
}

Кроме того, как только я создам класс типов таким образом, как мне убедиться, что для моих специализированных методов используется правильный класс типов вместо общей версии (которая, если она действительно общая, вероятно, должна компилироваться и, безусловно, будет работать, кроме того, что это не то, что я хочу)?

Есть ли способ сделать это без макросов? С макросами проще?


person Rex Kerr    schedule 06.04.2015    source источник


Ответы (3)


Пока это моя лучшая попытка. Это работает, но реализация не очень (даже если результаты). Улучшения приветствуются!

Существует способ сделать это без макросов, как на уровне класса, так и на уровне метода, и он включает классы типов — их довольно много! И ответ не совсем одинаков для классов и методов. Так что терпите меня.

Специализированные классы вручную

Вы вручную специфицируете классы так же, как вы вручную предоставляете любую различную реализацию для классов: ваш суперкласс является абстрактным (или является трейтом), а подклассы предоставляют детали реализации.

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,

  1. Сделайте специализированный класс абстрактным и вручную предоставьте конкретные реализации
  2. Сделать так, чтобы специализированные методы (включая генераторы для специализированного класса) принимали классы типов, выполняющие реальную работу.
  3. Сделайте черту базового класса типов @specialized и предоставьте конкретные реализации
  4. Предоставьте неявные vals или defs в иерархии наследования признаков, чтобы выбрать правильный.

Это много шаблонов, но в конце концов вы получаете безупречный специализированный опыт.

person Rex Kerr    schedule 06.04.2015
comment
Спасибо! отличные вопросы и ответы. один вопрос, хотя (извините, если это звучит глупо), но зачем проходить через все эти проблемы? разве это не возможно с сопоставлением с образцом? например: a match {case i: Int => i+1 ; case _ => a.toString} какие преимущества я могу получить, используя специализированный? это просто производительность? (если да, то как это соотносится с обычными проверками типов во время выполнения с сопоставлением с образцом? если нет, то с чем еще?). - person gilad hoch; 06.04.2015
comment
@giladhoch - это дорого во время выполнения и работает только для простых типов. Если у вас есть что-то вроде Foo[Int], возможно, вы не сможете сказать, что параметр равен Int (а если и сможете, то из-за глубокого знания того, как реализован Foo, например, с помощью этой схемы). Таким образом, за исключением самого простого случая, он одновременно и медленный, и хрупкий, если вообще возможно. - person Rex Kerr; 06.04.2015

Это ответ из списка рассылки о внутренних компонентах Scala:

С специализацией на минибоксе вы можете использовать функцию отражения:

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)
  }
}

Таким образом, вы можете выбрать точное поведение метода bippy на основе аргументов типа без определения каких-либо неявных классов.

person Community    schedule 27.05.2015

Я знаю, что он довольно старый, но я наткнулся на него в поисках чего-то другого, и, возможно, он окажется полезным. У меня была аналогичная мотивация, и я ответил на нее в как проверить, что я нахожусь внутри специализированной функции или класса

Я использовал таблицу обратного просмотра — SpecializedKey — это специализированный класс, который равен всем другим экземплярам с той же специализацией, поэтому я могу выполнить проверку следующим образом.

def onlyBytes[@specialized E](arg :E) :Option[E] =
    if (specializationFor[E]==specializationFor[Byte]) Some(arg)
    else None

Конечно, при работе с отдельными примитивными значениями выигрыша в производительности нет, но с коллекциями, особенно с итераторами, это становится полезным.

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)
}
person Turin    schedule 23.07.2016