如何使用Scala专业化提供手动专用实现?

Rex*_*err 24 scala specialization

专业化有望为原始类型提供高效率的实现,只需极少的额外样板.但专业化似乎过于渴望自己的利益.如果我想专门化一个类或方法,

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

class Bar[@specialized(Int) B] {
  var b: B = ???
  def baz: B = ???
}
Run Code Online (Sandbox Code Playgroud)

那么我需要编写一个涵盖专用和通用案例的实现.如果这些情况彼此真的不同,那么实现不会重叠怎么办?例如,如果我想在字节上执行数学运算,我需要& 0xFF在逻辑中插入一堆s.

我可以编写一个专门的类型类来正确地进行数学运算,但是这不会只是将同一个问题推回一个级别吗?如何+以不与更一般的实现冲突的方式为该类型类编写专用方法?

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

此外,一旦我以这种方式创建一个类类,我如何确保正确的类型类用于我的专用方法而不是通用版本(如果它是真正的通用,应该可能编译,当然会运行,除了它不是我想要的)?

有没有办法在没有宏的情况下做到这一点?使用宏更容易吗?

Rex*_*err 25

到目前为止,这是我最好的尝试.它工作但实现不漂亮(即使结果是).欢迎改进!

在类和方法级别都有一种无宏的方法可以做到这一点,它确实涉及类型类 - 相当多的类型!对于类和方法,答案并不完全相同.所以忍受我.

手动专业课程

您手动专门化类的方式与手动为类提供任何类型的不同实现的方式相同:您的超类是抽象的(或者是特征),子类提供实现细节.

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 }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我们只有一个专门的方法来选择正确的实现,我们就完成了:

object Bippy{
  def apply[@specialized(Int) B](initial: B) = ???  // Now what?
}
Run Code Online (Sandbox Code Playgroud)

因此,我们将提供自定义专用类方法的问题转换为需要提供自定义专用方法.

手动专业方法

手动专门化方法需要一种方法来编写一个实现,但仍然可以选择所需的实现(在编译时).类型类很棒.假设我们已经有了实现我们所有功能的类型类,并且编译器会选择正确的类型.然后我们就可以写了

def foo[@specialized(Int) A: SpecializedFooImpl](a: A): String =
  implicitly[SpecializedFooImpl[A]](a)
Run Code Online (Sandbox Code Playgroud)

...或者我们可以implicitly保证保留专业化,如果我们只想要一个类型参数.一般来说这些事情都不正确,所以我们将类型类写为隐式参数,而不是依赖于A: TC语法糖.

def foo[@specialized(Int) A](a: A)(implicit impl: SpecializedFooImpl[A]): String =
  impl(a)
Run Code Online (Sandbox Code Playgroud)

(实际上,无论如何,这都是较少的样板.)

因此,我们将提供自定义专用方法的问题转换为只需要编写专门的类型类并让编译器填写正确的类型.

手动专业类型类

类型类只是类,现在我们必须再次编写专门的类,但是有一个关键的区别. 用户不是要求任意实例的用户. 这为我们提供了足够的灵活性.

因为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)
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以创建implicits来提供这样的类型类

implicit def specFooAsAny[A] = new SpecFooImplAny[A]

implicit val specFooAsInt = new SpecFooImplInt
Run Code Online (Sandbox Code Playgroud)

除了我们有问题:如果我们真的试图打电话foo: Int,两个暗示将适用.因此,如果我们只是想方设法确定我们选择哪种类型的优先级,那么我们就可以了.

类型类的选择(以及一般的含义)

编译器用于确定隐式使用的权利的一个秘密因素是继承.如果implicits来自Avia B extends A,但B 声明它自己也可以应用,那些B胜利,如果所有其他条件相同.因此,我们将我们想要在继承层次结构中更深入地获胜.

此外,由于您可以自由定义特征中的含义,因此您可以将它们混合在任何地方.

因此,我们的最后一个难题是将我们的类型类隐含到一个相互扩展的特征链中,其中更通用的特征出现在前面.

trait LowPriorityFooSpecializers {
  implicit def specializeFooAsAny[A] = new SpecializedFooImplAny[A]
}

trait FooSpecializers extends LowPriorityFooSpecializers {
  implicit val specializeFooAsInt = new SpecializedFooImplInt
}
Run Code Online (Sandbox Code Playgroud)

将优先级最高的特征混合到需要隐含的位置,并根据需要选择类型类.

请注意,即使未使用专用注释,类型类也将与创建它们一样专门化.所以你可以完全不用specialized,只要你知道类型就足够了,除非你想使用专门的函数或与其他专业类互操作.(你可能会这样做.)

一个完整的例子

假设我们想要创建一个双参数专用bippy函数,它将应用以下转换:

bippy(a, b) -> b
bippy(a, b: Int) -> b+1
bippy(a: Int, b) -> b
bippy(a: Int, b: Int) -> a+b
Run Code Online (Sandbox Code Playgroud)

我们应该能够用三种类型和一种专门的方法来实现这一点.我们先尝试一下方法:

def bippy[@specialized(Int) A, @specialized(Int) B](a: A, b: B)(implicit impl: SpecBippy[A, B]) =
  impl(a, b)
Run Code Online (Sandbox Code Playgroud)

然后是类型类:

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
}
Run Code Online (Sandbox Code Playgroud)

然后是链式特征的含义:

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
}
Run Code Online (Sandbox Code Playgroud)

最后我们将尝试一下(在将所有内容粘贴: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
Run Code Online (Sandbox Code Playgroud)

它有效 - 我们的自定义实现已启用.只是为了检查我们可以使用任何类型,但我们不会泄漏到错误的实现:

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
Run Code Online (Sandbox Code Playgroud)

最后,为了验证我们实际上已经避免了拳击,我们将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
Run Code Online (Sandbox Code Playgroud)

因此我们可以看到我们专业的bippy-adder实现了与专用Function2相同的性能(每ns大约增加3个,这对于现代机器来说是正确的).

摘要

要使用@specialized注释编写自定义专用代码,

  1. 使专门的类抽象并手动提供具体的实现
  2. 使专门的方法(包括专门的类的生成器)采用完成实际工作的类型类
  3. 创建基类型类特征@specialized并提供具体实现
  4. 在特征的继承层次结构中提供隐式val或def,以便选择正确的值

这是很多样板,但最后你可以获得无缝的定制专业体验.