29

Scalaでジェネリック関数(またはクラス)を特殊化することは可能ですか?たとえば、データをByteBufferに書き込むジェネリック関数を記述したいとします。

def writeData[T](buffer: ByteBuffer, data: T) = buffer.put(data)

ただし、putメソッドは1バイトしか使用せず、それをバッファーに入れるため、次のようにIntとLongsに特化する必要があります。

def writeData[Int](buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData[Long](buffer: ByteBuffer, data: Long) = buffer.putLong(data)

コンパイルされません。もちろん、代わりに3つの異なる関数writeByte、writeInt、writeLongをそれぞれ書き込むこともできますが、配列に別の関数があるとしましょう。

def writeArray[T](buffer: ByteBuffer, array: Array[T]) {
  for (elem <- array) writeData(buffer, elem)
}

これは、特殊なwriteData関数がないと機能しません。別の関数のセットwriteByteArray、writeIntArray、writeLongArrayをデプロイする必要があります。タイプに依存する書き込み関数を使用する必要があるときはいつでも、この状況に対処しなければならないのはクールではありません。私はいくつかの調査を行いましたが、考えられる回避策の1つは、パラメーターのタイプをテストすることです。

def writeArray[T](buffer: ByteBuffer, array: Array[T]) {
  if (array.isInstanceOf[Array[Byte]])
    for (elem <- array) writeByte(buffer, elem)
  else if (array.isInstanceOf[Array[Int]])
    for (elem <- array) writeInt(buffer, elem)
  ...
}

これは機能する可能性がありますが、特殊な機能バージョンとは異なり、タイプチェックは実行時に実行されるため、効率が低下します。

だから私の質問は、ScalaまたはJavaでこの種の問題を解決するための最も望ましくて好ましい方法は何ですか?よろしくお願いします!

4

4 に答える 4

22

Wouldn't it be nice if you could have both a compact and efficient solution? It turns out that you can, given Scala's @specialized feature. First a warning: the feature is somewhat buggy, and may break if you try to use it for something too complicated. But for this case, it's almost perfect.

The @specialized annotation creates separate classes and/or methods for each primitive type, and then calls that instead of the generic version whenever the compiler knows for sure what the primitive type is. The only drawback is that it does all of this completely automatically--you don't get to fill in your own method. That's kind of a shame, but you can overcome the problem using type classes.

Let's look at some code:

import java.nio.ByteBuffer
trait BufferWriter[@specialized(Byte,Int) A]{
  def write(b: ByteBuffer, a: A): Unit
}
class ByteWriter extends BufferWriter[Byte] {
  def write(b: ByteBuffer, a: Byte) { b.put(a) }
}
class IntWriter extends BufferWriter[Int] {
  def write(b: ByteBuffer, a: Int) { b.putInt(a) }
}
object BufferWriters {
  implicit val byteWriter = new ByteWriter
  implicit val intWriter = new IntWriter
}

This gives us a BufferWriter trait which is generic, but we override each of the specific primitive types that we want (in this case Byte and Int) with an appropriate implementation. Specialization is smart enough to link up this explicit version with the hidden one it normally uses for specialization. So you've got your custom code, but how do you use it? This is where the implicit vals come in (I've done it this way for speed and clarity):

import BufferWriters._
def write[@specialized(Byte,Int) A: BufferWriter](b: ByteBuffer, ar: Array[A]) {
  val writer = implicitly[BufferWriter[A]]
  var i = 0
  while (i < ar.length) {
    writer.write(b, ar(i))
    i += 1
  }
}

The A: BufferWriter notation means that in order to call this write method, you need to have an implicit BufferWriter[A] handy. We've supplied them with the vals in BufferWriters, so we should be set. Let's see if this works.

val b = ByteBuffer.allocate(6)
write(b, Array[Byte](1,2))
write(b, Array[Int](0x03040506))
scala> b.array
res3: Array[Byte] = Array(1, 2, 3, 4, 5, 6)

If you put these things in a file and start poking around the classes with javap -c -private you'll see that the appropriate primitive methods are being used.

(Note that if you didn't use specialization, this strategy would still work, but it would have to box values inside the loop to copy the array out.)

于 2012-11-01T13:24:38.230 に答える
16

型クラス パターンを使用します。これには、instanceOf チェック (またはパターン マッチング) よりもタイプセーフであるという利点があります。

import java.nio.ByteBuffer

trait BufferWriter[A] {
  def write(buffer: ByteBuffer, a: A)
}

class BuffPimp(buffer: ByteBuffer) {
  def writeData[A: BufferWriter](data: A) = { 
    implicitly[BufferWriter[A]].write(buffer, data)
  }
}

object BuffPimp {
  implicit def intWriter = new BufferWriter[Int] {
    def write(buffer: ByteBuffer, a: Int) = buffer.putInt(a)
  }
  implicit def doubleWriter = new BufferWriter[Double] {
    def write(buffer: ByteBuffer, a: Double) = buffer.putDouble(a)
  }
  implicit def longWriter = new BufferWriter[Long] {
    def write(buffer: ByteBuffer, a: Long) = buffer.putLong(a)
  }
  implicit def wrap(buffer: ByteBuffer) = new BuffPimp(buffer)
}

object Test {
  import BuffPimp._
  val someByteBuffer: ByteBuffer
  someByteBuffer.writeData(1)
  someByteBuffer.writeData(1.0)
  someByteBuffer.writeData(1L)
}

したがって、このコードは型クラスの最適なデモではありません。私はまだ彼らにとても慣れていません。このビデオでは、その利点とその使用方法について非常に確かな概要を説明しています: http://www.youtube.com/watch?v=sVMES4RZF-8

于 2012-11-01T09:01:35.450 に答える
3
  1. 宣言

    def writeData[Int](buffer: ByteBuffer, data: Int) 
    def writeData[Long](buffer: ByteBuffer, data: Long)
    

formalIntとLongは型パラメーターであり、標準のScala型ではないため、これらは同等であるため、コンパイルしないでください。標準のScalaタイプで関数を定義するには、次のように記述します。

def writeData(buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData(buffer: ByteBuffer, data: Long) = buffer.putLong(data)

このようにして、同じ名前で異なる関数を宣言します。

  1. これらは異なる関数であるため、静的に不明なタイプのリストの要素に適用することはできません。最初にリストのタイプを決定する必要があります。リストのタイプがAnyRefである可能性があることに注意してください。その後、各要素のタイプを動的に決定します。決定は、元のコードと同じように、または提案されているようisInstanceOfにパターンマッチングを使用して行うことができます。rolveこれで同じバイトコードが生成されると思います。

  2. 要約すると、次を選択する必要があります。

    • などのような複数の関数を持つ高速コードwriteByteArray, writeIntArray。それらはすべて同じ名前を持つことwriteArrayができますが、実際のパラメーターによって静的に区別できます。DominicBou-Saによって提案されたバリアントはこの種のものです。

    • 実行時型の決定を伴う簡潔で遅いコード

残念ながら、高速で簡潔なコードを使用することはできません。

于 2012-11-01T09:53:05.170 に答える
2

これはどう:

def writeData(buffer: ByteBuffer, data: AnyVal) {
  data match {
    case d: Byte => buffer put d
    case d: Int  => buffer putInt d
    case d: Long => buffer putLong d
    ...
  }
}

ここでは、メソッドで大文字と小文字を区別しますwriteData。これにより、以降のすべてのメソッドが非常に単純になります。

def writeArray(buffer: ByteBuffer, array: Array[AnyVal]) {
  for (elem <- array) writeData(buffer, elem)
}

利点:シンプル、短い、わかりやすい。

短所:すべての型を処理しないと、完全には型安全ではありませんAnyVal: 誰かがwriteData(buffer, ())(2 番目の引数が typeUnitである) を呼び出す可能性があり、実行時にエラーが発生する可能性があります。しかし()、問題を解決する no-op の処理を​​行うこともできます。完全なメソッドは次のようになります。

def writeData(buffer: ByteBuffer, data: AnyVal) {
  data match {
    case d: Byte   => buffer put d
    case d: Short  => buffer putShort d
    case d: Int    => buffer putInt d
    case d: Long   => buffer putLong d
    case d: Float  => buffer putFloat d
    case d: Double => buffer putDouble d
    case d: Char   => buffer putChar d
    case true      => buffer put 1.asInstanceOf[Byte]
    case false     => buffer put 0.asInstanceOf[Byte]
    case ()        =>
  }
}

ちなみに、Scala の厳密なオブジェクト指向の性質により、これは簡単に機能します。プリミティブ型がオブジェクトではない Java では、これははるかに面倒です。そこでは、醜いボックス化とボックス化解除を行いたくない場合を除き、プリミティブ型ごとに個別のメソッドを実際に作成する必要があります。

于 2012-11-01T08:57:46.593 に答える