Автоматически использовать хэшированные классы

Я ищу способ иметь классы, которые ведут себя как классы case, но это автоматически hash consed.

Одним из способов достижения этого для целых списков будет:

import scala.collection.mutable.{Map=>MutableMap}

sealed abstract class List
class Cons(val head: Int, val tail: List) extends List
case object Nil extends List

object Cons {
  val cache : MutableMap[(Int,List),Cons] = MutableMap.empty
  def apply(head : Int, tail : List) = cache.getOrElse((head,tail), {
    val newCons = new Cons(head, tail)
    cache((head,tail)) = newCons
    newCons
  })
  def unapply(lst : List) : Option[(Int,List)] = {
    if (lst != null && lst.isInstanceOf[Cons]) {
      val asCons = lst.asInstanceOf[Cons]
      Some((asCons.head, asCons.tail))
    } else None
  }
}

И, например, while

scala> (5 :: 4 :: scala.Nil) eq (5 :: 4 :: scala.Nil)
resN: Boolean = false

получаем

scala> Cons(5, Cons(4, Nil)) eq Cons(5, Cons(4, Nil))
resN: Boolean = true

Теперь то, что я ищу, - это общий способ достичь этого (или что-то очень похожее). В идеале, я не хочу печатать гораздо больше, чем:

class Cons(val head : Int, val tail : List) extends List with HashConsed2[Int,List]

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

Ответ 1

Вы можете определить несколько признаков InternableN[Arg1, Arg2, ..., ResultType] для N, где количество аргументов apply(): Internable1[A,Z], Internable2[A,B,Z] и т.д. Эти признаки определяют сам кеш, метод intern() и apply метод, который мы хотим захватить.

Мы должны определить признак (или абстрактный класс), чтобы гарантировать ваши черты InternableN, что действительно существует метод приложения, который можно переопределить, позвоните ему Applyable.

trait Applyable1[A, Z] {
  def apply(a: A): Z
}
trait Internable1[A, Z] extends Applyable1[A, Z] {
  private[this] val cache = WeakHashMap[(A), Z]()
  private[this] def intern(args: (A))(builder: => Z) = {
    cache.getOrElse(args, {
      val newObj = builder
      cache(args) = newObj
      newObj
    })
  }
  abstract override def apply(arg: A) = {
    println("Internable1: hijacking apply")
    intern(arg) { super.apply(arg) }
  }
}

Сопутствующий объект вашего класса должен быть смешением конкретного класса, реализующего ApplyableN с InternableN. Было бы нецелесообразно применять прямое определение в вашем сопутствующем объекте.

// class with one apply arg 
abstract class SomeClassCompanion extends Applyable1[Int, SomeClass] {
  def apply(value: Int): SomeClass = {
    println("original apply")
    new SomeClass(value)
  }
}
class SomeClass(val value: Int)
object SomeClass extends SomeClassCompanion with Internable1[Int, SomeClass]

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

Все это может (и должно) также быть определено для классов с более чем одним аргументом. Для случая с двумя аргументами:

trait Applyable2[A, B, Z] {
  def apply(a: A, b: B): Z
}
trait Internable2[A, B, Z] extends Applyable2[A, B, Z] {
  private[this] val cache = WeakHashMap[(A, B), Z]()
  private[this] def intern(args: (A, B))(builder: => Z) = {
    cache.getOrElse(args, {
      val newObj = builder
      cache(args) = newObj
      newObj
    })
  }
  abstract override def apply(a: A, b: B) = {
    println("Internable2: hijacking apply")
    intern((a, b)) { super.apply(a, b) }
  }
}

// class with two apply arg 
abstract class AnotherClassCompanion extends Applyable2[String, String, AnotherClass] {
  def apply(one: String, two: String): AnotherClass = {
    println("original apply")
    new AnotherClass(one, two)
  }
}
class AnotherClass(val one: String, val two: String)
object AnotherClass extends AnotherClassCompanion with Internable2[String, String, AnotherClass]

Взаимодействие показывает, что метод применения Internables выполняется до исходного apply(), который запускается только при необходимости.

scala> import SomeClass._
import SomeClass._

scala> SomeClass(1)
Internable1: hijacking apply
original apply
res0: SomeClass = [email protected]

scala> import AnotherClass._
import AnotherClass._

scala> AnotherClass("earthling", "greetings")
Internable2: hijacking apply
original apply
res1: AnotherClass = [email protected]

scala> AnotherClass("earthling", "greetings")
Internable2: hijacking apply
res2: AnotherClass = [email protected]

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

Короткодоступный код как github gist.

Ответ 2

Возможно, немного взломанный, но вы можете попробовать определить свой собственный метод intern(), например Java String:

import scala.collection.mutable.{Map=>MutableMap}

object HashConsed {
  val cache: MutableMap[(Class[_],Int), HashConsed] = MutableMap.empty
}

trait HashConsed {
  def intern(): HashConsed = 
    HashConsed.cache.getOrElse((getClass, hashCode), {
      HashConsed.cache((getClass, hashCode)) = this
      this
    })
}

case class Foo(bar: Int, baz: String) extends HashConsed

val foo1 = Foo(1, "one").intern()
val foo2 = Foo(1, "one").intern()

println(foo1 == foo2) // true
println(foo1 eq foo2) // true