Подтвердить что ты не робот

Специализация общих функций в Scala (или Java)

Можно ли специализировать общие функции (или класс) в Scala? Например, я хочу написать общую функцию, которая записывает данные в ByteBuffer:

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

Но поскольку метод put принимает только байт и помещает его в буфер, мне нужно специализировать его для Ints and 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. Чтобы иметь дело с ситуацией таким образом, когда мне нужно использовать зависящие от типа функции записи, это не круто. Я сделал некоторые исследования, и один из возможных способов обхода - проверить тип параметра:

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? Я ценю вашу помощь заранее!

4b9b3361

Ответ 1

Было бы неплохо, если бы у вас было как компактное, так и эффективное решение? Оказывается, вы можете использовать функцию Scala @specialized. Сначала предупреждение: функция несколько глючит, и может сломаться, если вы попытаетесь использовать ее для чего-то слишком сложного. Но для этого случая это почти идеально.

Аннотация @specialized создает отдельные классы и/или методы для каждого примитивного типа, а затем вызывает это вместо общей версии, когда компилятор точно знает, что такое примитивный тип. Единственным недостатком является то, что он делает все это полностью автоматически - вы не можете заполнить свой собственный метод. Такой позор, но вы можете преодолеть проблему с помощью классов типов.

Посмотрим на некоторый код:

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
}

Это дает нам признак BufferWriter, который является общим, но мы переопределяем каждый из необходимых нам примитивных типов (в данном случае Byte и Int) с соответствующей реализацией. Специализация достаточно умна, чтобы связать эту явную версию со скрытой, которую она обычно использует для специализации. Итак, у вас есть собственный код, но как вы его используете? Именно здесь появляются неявные vals (я сделал это так для скорости и ясности):

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

Обозначение A: BufferWriter означает, что для вызова этого метода write вам нужно иметь неявный BufferWriter[A] удобный. Мы снабдили их vals в BufferWriters, поэтому мы должны быть установлены. Посмотрим, работает ли это.

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)

Если вы поместите эти вещи в файл и начнете прокручивать классы с помощью javap -c -private, вы увидите, что используются соответствующие примитивные методы.

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

Ответ 2

Используйте шаблон типа. Это имеет преимущество перед instanceOf проверки (или соответствия шаблонов) типа typesafe.

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

Ответ 3

  • Объявления

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

не компилируются, потому что они эквивалентны, так как Int и Long являются параметрами типа formal, а не стандартными типами Scala. Чтобы определить функции со стандартными типами Scala, просто напишите:

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

Таким образом вы объявляете разные функции с тем же именем.

  • Поскольку они являются разными функциями, вы не можете применять их к элементам списка статически неизвестного типа. Сначала вы должны определить тип списка. Обратите внимание, что может случиться, что тип List - AnyRef, тогда вы динамически определяете тип каждого элемента. Определение можно сделать с помощью isInstanceOf, как в вашем исходном коде, или с помощью сопоставления с образцом, как предлагается rolve. Я думаю, что это приведет к тому же байт-коду.

  • В сумме вам нужно выбрать:

    • быстрый код с несколькими функциями, такими как writeByteArray, writeIntArray и т.д. Все они могут иметь одно и то же имя writeArray, но могут быть статически выделены их фактическими параметрами. Вариант, предложенный Домиником Бу-Са, относится к этому типу.

    • краткий, но медленный код с определением типа времени выполнения

К сожалению, вы не можете иметь быстрый и сжатый код.

Ответ 4

Как насчет этого:

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, ()) (второй аргумент имеет тип Unit), что может привести к во время выполнения. Но вы также можете сделать обработку () 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, где примитивные типы не являются объектами, это было бы намного более громоздким. Там вам действительно нужно создать отдельный метод для каждого примитивного типа, если вы не захотите сделать какой-то уродливый бокс и unboxing.