Let’s unpack some (proposed) Scala 3

Mark "Justin" Waks
7 min readSep 20, 2018

In his recent proposal for extension method syntax, Martin provides a lovely example of some library code that could be written in Scala 3. It illustrates a whole bunch of new concepts coming in the next major rev of the language. So let’s take that example in hand, and walk through how it works, in gory detail. (This is mainly intended for folks who haven’t already been following the conversations about upcoming features.)

Important: some of this is just proposals at this point — in particular, this is just one proposal among several of how extension methods might look. All of this is subject to change. Remember that Scala 3 is still experimental and under-development, and will be for a little while yet. (The actual release of Scala 3 is currently planned for 2020, as far as I know.) See the current Dotty documentation for a better idea of what is considered reasonably likely to make it into the language. (“Dotty” is the original codename for Scala 3, and is still often used for it.)

Here’s the example that we’ll dissect:

object PostConditions {
opaque type WrappedResult[T] = T

private object WrappedResult {
def wrap[T](x: T): WrappedResult[T] = x
def unwrap[T](x: WrappedResult[T]): T = x
}

def result[T](implicit er: WrappedResult[T]): T =
WrappedResult.unwrap(er)

implicit object Ensuring {
def (x: T).ensuring[T](
condition: implicit WrappedResult[T] => Boolean
): T = {
implicit val wrapped = WrappedResult.wrap(x)
assert(condition)
x
}
}
}

object Test {
import PostConditions._
val s = List(1, 2, 3).sum.ensuring(result == 6)
}

What We’re Trying to Get

The whole point of the exercise is that last clause being tested:

sum.ensuring(result == 6)

That is:

  • From any value, we can add an ensuring clause.
  • ensuring takes a predicate function that we are asserting is true.
  • Inside that predicate, we can use result to get at the value we are testing.

Put together, this is a “post-condition” — a test that, after doing something, we have an expected result.

Keep in mind, that automatic definition of result is the only bit that’s new and cool. We could easily write this today, if we were willing to accept a call-site of:

sum.ensuring(result => result == 6)

And yes, in some cases we can use the even simpler:

sum.ensuring(_ == 6)

But the new machinery is much more powerful and flexible. So let’s take apart all the pieces that are involved here.

Extension Methods

First up is the current proposal for Extension Methods. (Remember: this is only one proposal among several, but I think there’s a lot of enthusiasm for some sort of Extension Methods in Scala 3, however the syntax plays out.)

implicit object Ensuring {
def (x: T).ensuring[T]( ... ): T = { ... }
}

Here’s the signature of ensuring. It is declared inside of an implicit object, so any code that can see that object — because it is below the implicit or has imported it — will automatically make these functions available.

The syntax def (x: T).ensuring[T] is the proposal at hand. This says that, for any type T, it now acts as if it has a method named ensuring. This means that we can call:

sum.ensuring(...)

and have that work — even though sum is an Int, which doesn’t have an ensuring method.

Opaque Types

Next, let’s look at this bit:

opaque type WrappedResult[T] = T

private object WrappedResult {
def wrap[T](x: T): WrappedResult[T] = x
def unwrap[T](x: WrappedResult[T]): T = x
}

Opaque Types are a new concept in Scala, which will probably (fingers crossed) land in the Scala 2.13 series, so they aren’t technically a Dotty feature, but they’re still new and exciting. They’re a way to define your own type that is actually some other type under the hood.

This is really important. Scala is all about strongly-typed code — if you don’t care about strong types, there are plenty of dynamic languages that you’re likely to enjoy more. But there’s still a nasty habit of using the common types (especially Int and String) in place of all sorts of other types. For example, you will often see something like:

val url: String = "http://medium.com"

or:

type Flavor = Int
val vanilla: Flavor = 1
val chocolate: Flavor = 2
val strawberry: Flavor = 3

These lose most of the benefit of strong types. In particular, the primary value of strong types is that the compiler will tell you when you do something wrong. In the above, you could easily say:

val jabberwocky = "Twas brillig, and the slithy toves..."
...
val url: String = jabberwocky

which is, clearly, nonsense — but a String is a String, so the compiler can’t tell that you’re putting a non-URL where a URL belongs.

And while the second example looks better, with that Flavor type there, that’s really just an alias for Int with no limitations, so I can say:

val special: Int = 42
val myFavorite: Flavor = special

What we want are strong types for URL and Flavor. But we’d like to be able to do that efficiently, without primitives like Int being boxed into objects. And we’d like nice strong walls around those types, so you can’t accidentally combine one type with another.

That’s where opaque types come in. An opaque type is simply an alias for another type, similar to Flavor above, but with much stronger compiler enforcement. For example, look at the declaration:

opaque type WrappedResult[T] = T

So a WrappedType[T] is nothing more than T itself, with no extra baggage. But the compiler knows that they are different types, so if I try to say:

def foo(x: WrappedResult[Int]) {
val y: Int = x // compile error
}

It doesn’t work, because the compiler won’t let you arbitrarily mix and match T with WrappedResult[T]. There’s no boxing or other overhead at runtime, but from the compiler’s point of view they’re separate types.

You can’t add methods directly to an opaque type, since it is just another name for the type it is wrapping. But you can put them into its companion object:

object WrappedResult {
def wrap[T](x: T): WrappedResult[T] = x
def unwrap[T](x: WrappedResult[T]): T = x
}

(We’re going to ignore the private from the original definition, which isn’t relevant here.) In the companion object, you define the operations for the opaque type. The companion object is slightly magical, in that it is the one place that knows (from the compiler’s perspective) that there are really the same type, and can be mixed and matched. So this is where you put constructors, as well as other methods on your wrapped type.

What about AnyVal?

Some folks are likely to object that we already have this capability, in the form of AnyVal types. We could have said:

case class Flavor(v: Int) extends AnyVal
val vanilla: Flavor = Flavor(1)
val chocolate: Flavor = Flavor(2)
val strawberry: Flavor = Flavor(3)

Isn’t that good enough? Not really. The problem is, AnyVal tries to avoid boxing, but frequently fails — there are a lot of circumstances in which you will find that it has accidentally boxed a whole lot of values, and your performance is far worse than you expected.

It’s also rather porous by default: unless you specifically declare v to be private above, that underlying value is available for anybody to snarf and misuse.

So opaque types are a better solution to this problem. The efficiency is better guaranteed, and the type walls are much stronger.

Implicit Function Types

Finally, we have implicit functions, which are one of the subtler but more important enhancements in Dotty. Let’s look at ensuring in more detail now:

def result[T](implicit er: WrappedResult[T]): T = 
WrappedResult.unwrap(er)
def (x: T).ensuring[T](
condition: implicit WrappedResult[T] => Boolean
): T = {
implicit val wrapped = WrappedResult.wrap(x)
assert(condition)
x
}

It’s worth looking at the call site again:

val s = List(1, 2, 3).sum.ensuring(result == 6)

Looking at that call to ensuring, we would traditionally expect it to be declared as a by-name parameter, like this:

def (x: T).ensuring[T](condition: => Boolean): T

That would take a function that returns a Boolean, but doesn’t let us pass our result in.

Or we might traditionally pass in our value explicitly:

def (x: T).ensuring[T](condition: T => Boolean): T

That would let us write something like:

val s = List(1, 2, 3).sum.ensuring(r => r == 6)

But instead, we have something new:

def (x: T).ensuring[T](
condition: implicit WrappedResult[T] => Boolean
): T

The parameter is a function that expects an implicit parameter of type WrappedResult[T], and returns a Boolean. The call site doesn’t have to declare the parameter — it’s implicit, after all — but that parameter is available when something inside summons that type implicitly.

In other words, at the call site:

val s = List(1, 2, 3).sum.ensuring(result == 6)

Inside that call to ensuring, we now have an implicit WrappedResult[Int], available for anything inside that wants to use it.

How do we use that? Let’s look at the definition of result again:

def result[T](implicit er: WrappedResult[T]): T = 
WrappedResult.unwrap(er)

result is a function that takes a WrappedResult[T], and unwraps it. So it’s a function that only works inside of ensuring, since that is the only place that defines WrappedResult[T].

That’s the real power of implicit functions — it’s a technique for passing values, in a strongly-typed and well-controlled way, from outer function calls to inner ones.

So now let’s really walk through ensuring:

def (x: T).ensuring[T](
condition: implicit WrappedResult[T] => Boolean
): T = {
implicit val wrapped = WrappedResult.wrap(x)
assert(condition)
x
}
  • This takes the value x, that we have extended with the .ensuring() method.
  • It wraps it into a WrappedResult (which, remember, doesn’t involve boxing, so this is free at runtime).
  • It makes that implicit, and then calls condition, which expects that implicit value.
  • At the call site, result unwraps the value so that we can compare it and return our Boolean result.
  • Finally, we assert on that result.

Conclusion

Putting it all together, we have three new tools coming to Scala:

  • Extension methods (whatever the syntax turns out to be), providing better ways to add new methods to existing types.
  • Opaque types, which make it easier to strengthen the types in our programs with little or no overhead.
  • Implicit function types, which let us pass values down chains of library functions without boilerplate at the call site.

The above is basically a small toy example, but these capabilities are going to make library building — and more importantly, library usage — considerably nicer down the road.

--

--

Mark "Justin" Waks

Lifelong programmer and software architect, specializing in online social tools and (nowadays) Scala. Architect of Querki (“leading the small data revolution”).