A Snippet of Dotty

In a recent PR, Martin highlighted the following code as “a great showcase how opaque types, implied instances, extension methods and inline can work together to give something beautiful.”

opaque type IArray[T] = Array[T]
object IArray {
implied arrayOps {
inline def (arr: IArray[T]) apply[T] (n: Int): T =
(arr: Array[T]).apply(n)
inline def (arr: IArray[T]) length[T] : Int =
(arr: Array[T]).length
}
  def apply[T: ClassTag](xs: T*): IArray[T] = Array(xs: _*)
...
}

This is showing off a lot of new concepts at once, so let’s take it apart, bit by bit. (I’ve talked about much of this before, so this post is less new-and-different and more about showing how it works together.)

Usual caveats: this reflects the current state of a pretty rapidly-evolving design. None of these features are guaranteed to make it into Scala 3, although all of them are now described in the Dotty documentation. Details are still likely to change. And keep in mind that I am not a member of the Dotty team, just a outside observer who is drinking the firehose from the repo, trying to keep up, and passing the interesting-looking bits on. (So don’t take any of this as gospel.)

Opaque Types

Let’s begin at the top:

opaque type IArray[T] = Array[T]

Opaque types are the incoming replacement for AnyVal. By now, we’re all used to this idiom:

case class Foo(b: Bar) extends AnyVal

In theory, this creates a low-overhead wrapper that hides the concept of Bar behind the concept of Foo. In practice, it has never worked very well. The wrapper is very permeable unless you take steps to avoid that — you can just say myFoo.b to get the Bar. And while in theory this doesn’t box, in practice it does so pretty often, so the “low-overhead” fails on a pretty regular basis.

opaque replaces that with a first-class notion of type hiding. In this case, the IArray type is a much stronger barrier — outside code really can’t get at the underlying Array except through methods you explicitly put into the companion object. For example, here’s one of the constructors:

def apply[T: ClassTag](xs: T*): IArray[T] = Array(xs: _*)

This is inside the companion object, so it can see the underlying relationship between IArray and Array. It is constructing an Array, and returning that as an IArray, with no need for asInstanceOf — in here, we know they’re the same type. But anywhere outside the companion object, they’re unrelated.

Also, the low-overhead guarantees are much stronger: this doesn’t box, it is simply a really strong alias for the underlying type.

Of all the features proposed for Scala 3, this is perhaps the one I am surest is going to go in, in roughly its current form. It was proposed quite a while ago by now, and just about everyone has agreed that it’s a big help, providing Scala with a powerful approach to type aliasing. It will help Scala 3 provide stronger and more appropriate types than ever before.

Implied Instances

Inside the companion object, we find:

object IArray {
implied arrayOps {
inline def (arr: IArray[T]) apply[T] (n: Int): T =

Implied instances are a new mechanism for specifying the functionality of a type. One of the goals being examined in Scala 3 is replacing the many uses of the word implicit with more-precise terminology and syntax.

In particular, this is replacing the heavily-used implicit class, which is currently used for extension methods. In Scala 2, the above would be stated as something like:

object IArray {
implicit class arrayOps(arr: IArray[T]) {
def apply[T](n: Int): T = ...
}
}

In the new model, this gets broken into two concepts, Implied Instances and Extension Methods.

Implied Instances are actually much more powerful than what you see here — this is basically the degenerate case of a new tool designed to make it easier to define typeclass instances. See the documentation for more details.

The syntax around implicits is probably one of the most under-discussion aspects of Dotty, and has changed a lot in the past few months. So watch this space — further changes wouldn’t surprise me.

Extension Methods

Now, let’s look at the innards of arrayOps:

    inline def (arr: IArray[T]) apply[T] (n: Int): T = 
(arr: Array[T]).apply(n)
inline def (arr: IArray[T]) length[T] : Int =
(arr: Array[T]).length

Notice the weird syntax of apply? Let’s strip away the stuff around it:

def (arr: IArray[T]) apply[T] (n: Int): T

This is the new extension method syntax. I can now put a parameter before the name of the function, which means, “this function should be used like a method on that first parameter”. So the above is effectively a new method on IArray[T], defined from the outside.

Taking the second of those, I can now say myIArray.length, just as if length was a method defined in the conventional way. The syntax reflects the way that you call it, with the type being operated on to the left of the name of the method you call, the same way that the value being operated on is to the left of the dot.

Inline

Finally, let’s look at those functions again:

    inline def (arr: IArray[T]) apply[T] (n: Int): T = 
(arr: Array[T]).apply(n)
inline def (arr: IArray[T]) length[T] : Int =
(arr: Array[T]).length

Note that they are declared as inline. This tells the compiler that it should aggressively inline the function body, instead of building and calling the function normally. The contents of the function will be placed at the call site.

That helps us achieve the low-overhead goal of our opaque type. We don’t even have a real function call here: the calls to myIArray.apply and myIArray.length are rewritten as calls to the underlying functions on Array. Since the only thing that happens inside of our functions are casts (which are free) and calls to the underlying functions, these functions wind up with no overhead at all — they are literally just calls to our underlying type.

Wrapping it Up

In this little code snippet, we find:

  • opaque lets you easily create type aliases that really hide their underlying types, so you can write more strongly-typed code.
  • Those aliases are inherently low-overhead, with no boxing.
  • You can use inline to reduce the overhead of functions on those opaque types, sometimes to zero.
  • You can use implied to make functions available for a type.
  • The extension-method syntax lets you add new “methods” to existing types from the outside, without the syntactic complexity of the old implicit class mechanism.

So yeah — while the details may change going forward, we are gradually moving towards tools that will make Scala 3 code more robust, easier to write, and more readable. “Beautiful” seems warranted…