8

I'd like to have type-safe "subclasses" of primitives in my Scala code without the performance penalty of boxing (for a very low-latency application). For example, something like this:

class Timestamp extends Long
class ProductId extends Long

def process(timestamp: Timestamp, productId: ProductId) {
  ...
}

val timestamp = 1: Timestamp // should not box
val productId = 1: ProductId // should not box

process(timestamp, productId) // should compile
process(productId, timestamp) // should NOT compile

There was a thread on the Scala User mailing list last year which seemed to conclude it wasn't possible without boxing, but I wonder if this is possible now in Scala 2.8.

Mike
  • 1,839
  • 1
  • 17
  • 25

5 Answers5

3

Why not use type aliases instead? I appreciate that they are not perfect (i.e. they do not solve your compilation issue), but they can make your code clearer without taking the performance hit?

type Timestamp = Long
type ProductId = Long

Then you can write methods which use the pimp my library pattern and let the JVM use escape analysis to remove the runtime overhead:

class RichTimestamp(self: Timestamp) {
  //whatever you want here
}

Note that all the usual caveats apply: unless you are extremely sure (because you are doing ultra-low-latency programming e.g.), the performance hit of using the boxed type is probably not a concern. I deal with systems processing tens of millions of inputs a day with no issues whatsoever!

lambdas
  • 3,990
  • 2
  • 29
  • 54
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • @Jesper With an alias, Timestamp is just *an alias* for Long. This works both ways and Long is thus the *actual type*. So: No. But it does "add intent". –  Nov 23 '10 at 02:51
  • 2
    Ok, but the original poster asked for a type-safe solution - aliases are not a type-safe solution. – Jesper Nov 23 '10 at 12:26
  • Indeed, this does _not_ introduce any type safety (only a rather useless "coder wishful intent"). Both types would be accepted where one is required, no type safety between the aliases arises from this alone. – matanster Mar 28 '15 at 10:24
2

The root of the Scala type hierarchy is Any with children AnyVal and Anyref. All of the integral types (like Long in your example) descend from AnyVal and you can't create subclasses in that side of the tree. Children of AnyVal represent low level types within the JVM. Type-erasure means that at runtime there really is no AnyVal anymore and if you could make Timestamp it would also be lost at runtime. You need boxing/unboxing as a place to store your wrapper type information.

case class Timestamp(ts: Long)

A good JVM can eliminate a lot of the boxing/unboxing overhead at runtime. For example, see Experiences with escape analysis enabled on the JVM

Community
  • 1
  • 1
Ben Jackson
  • 90,079
  • 9
  • 98
  • 150
  • 1
    I don't think escape analysis can help in my actual applications, since these primitives are stored as fields and as map keys. I see how at runtime everything is lost, but is there any theoretical reason the compiler couldn't guarantee type safety at compile time? – Mike Nov 21 '10 at 21:29
1

The concept of primitives (on the JVM) is that they are predefined and final, you cannot add further primitives to the JVM, only classes (java.lang.Object in Java or scala.AnyRef in Scala)...

The boxing/ unboxing of a wrapper, as proposed by Ben, case class Timestamp(ts: Long), shouldn't create a substantial performance penality.

Type aliases, type Timestamp = Long, are really aliases, so there is no way for the compiler to distinguish two aliases to the same type (Long).

0__
  • 66,707
  • 21
  • 171
  • 266
1

This kind of thing could be ensured by a plugin. The unsupported and not-working units plugin for Scala, after all, did something like it when it prevented distances to be added to durations.

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
1

This is now possible, as of 2.10, with value classes:

object ValueClasses {
  case class Timestamp(timestamp: Long) extends AnyVal
  case class ProductId(productId: Long) extends AnyVal

  def process(timestamp: Timestamp, productId: ProductId): Unit =
    println(s"$timestamp $productId")

  def main(args: Array[String]): Unit = {
    val timestamp = Timestamp(1) // should not box
    val productId = ProductId(1) // should not box

    process(timestamp, productId) // should compile
//  process(productId, timestamp) // should NOT compile
  }
}

The commented line produces:

type mismatch;
 found   : ValueClasses.ProductId
 required: ValueClasses.Timestamp
  process(productId, timestamp) // should NOT compile

I don't have an easy way to convince you that the boxing won't happen, since a simple example like this could have boxing optimized away by the compiler, but here's the byte code:

  public void process(long, long);
…

  public void main(java.lang.String[]);
    Code:
       0: lconst_1
       1: lstore_2
       2: lconst_1
       3: lstore        4
       5: aload_0
       6: lload_2
       7: lload         4
       9: invokevirtual #65                 // Method process:(JJ)V
      12: return
Mike
  • 323
  • 3
  • 13
  • 1
    Would you care to expand this a little bit? Perhaps an example, or explanation, or a quote from the article you link to? – Suma Jul 20 '18 at 19:39