Как можно обеспечить специализированные реализации вручную со специализацией 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 = ???
}

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

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

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

Таким образом, мы можем видеть, что наш специализированный двухпозиционный сумматор достигает той же производительности, что и специализированная функция Function2 (около 3 добавок в нс, что примерно подходит для современной машины).

Резюме

Чтобы написать специальный специализированный код, используя @specialized аннотаций,

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

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

Это ответ из списка рассылки внутренностей 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 метод, основанный на аргументах типа без определения каких-либо неявных классов.

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

Я использовал таблицу обратного просмотра - 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)
}
Другие вопросы по тегам