Connecting...

Pexels Photo 665214

What’s new in Scala 3 by Sinisa Louc

Pexels Photo 665214

Scala Backend Engineer Sinisa Louc has many years of experience in the industry and with Scala being his main programming language he has a keen interest in staying up to date. This is his interesting article on What's new in Scala 3 which is not long away now. 

 

"What’s new in Scala 3

Here’s a short digest for all those who are still unfamiliar with the changes and improvements that are coming in Dotty / Scala 3. I’m not presenting anything that’s not already been presented elsewhere; I’m merely taking a bunch of sources — Dotty documentation, conference talks, blogposts, Google groups, SIPs, pull requests, etc. — and combining them into a text that’s (hopefully) easy to read, not diving too deep into any of the upcoming changes, but still providing enough detail to get you all excited about them.

 

Intersections and unions

Type system is going through some major face-lifting.

First of all, we’re getting true union and intersection types. Now you might be thinking “what’s the big deal, intersection types are nothing but A with B, and we modeled union types without problems with constructs such as Either or subtyping hierarchies (e.g. Apple and Banana extend Fruit)”. That is true, we had those mechanisms and we will still have them, but intersection and union types are slightly different.

Let’s start with union types. A union type, expressed as A | B, represent a “true” union, as opposed to a disjoint union (also known as tagged union) that we have with Either, scalaz disjunction etc. Disjoint union clearly separates between the “left” and “right” case, but standard non-disjoint union doesn’t. This means that there’s no Left and Right; you can literally do this:

val a: A | B = new A()
val b: A | B = new B()

They are commutative, so A | B is the same as B | A.

To be perfectly honest, there are probably not going to be extremely many places where you will actually want to use a union type instead of Either, Option etc., but that’s because the latter are monads which come equipped with handy functions such as map, flatMap, filter etc. But unions will definitely come handy in domain modeling, where we often resorted to aforementioned subtyping hierarchies. This will be super-handy if you had a slightly complicated case of intersections on middle levels of the hierarchy; fore example, let’s say we have traits Dog, Bird and Bat. Then there are traits CanSee (only extended by Dog and Bird), CanFly (only Bird and Bat) and Mammal (only Dog and Bat). Or we don’t want them to sound so general because those two types are all that will ever extend them, so we name them more specifically DogOrBird, DogOrBat, BirdOrBat. Super clumsy. With Scala 3 you’ll be able to simply state your type as Dog | Bird without those intermediate types in the hierarchy.

Intersection types (expressed as A & B) represent a dual concept to union types, and just like with union types, there are already similar constructs present in Scala. We can already mix in traits B and C into some class/trait/object A, resulting in intersection type “A with B with C”. Main difference between such mixins and the intersection types we’re getting in Scala 3 is the commutativity: A with B is not the same as B with A, at least from the type system perspective, while A & B and B & A are the same thing and can be used interchangeably. Unlike disjoint unions and subclass hierarchies which still have advantages in certain use cases over Scala 3 unions, intersection types are basically commutative versions of standard mixins types. This means that “A with B with C” syntax will be deprecated in the future.

 

Trait parameters

As you know, traits cannot have parameters; we have abstract classes for that. However, abstract classes cannot be mixed into different parts of the class hierarchy.

Well, now the traits are getting parameters too, which means syntax like this:

trait Foo(val s: String) {
  ...
}

This could lead us to the “diamond problem” in class hierarchies, e.g. both Foo(“a”) and Foo(“b”) have been mixed in at some point. That would actually not compile. Rules are as follows:

  • Only classes can pass arguments to their parent traits; traits cannot pass arguments to traits.
  • When a class C extends a parameterized trait T, it must provide the arguments to T, except if C has a superclass that also extends T; in that case, superclass must provide the arguments, and C cannot.

This has been described quite nicely in the associated SIP. I’m pretty sure people will come up with various edge cases when using trait parameters, but let’s deal with those when the time comes. Birds-eye view of the feature looks pretty promising.

 

Function types

There are two main big things coming up regarding function types.

First, we are getting dependent function types. This means having types such as (a: A) => a.Foo. We already have dependent methods, but now we’re getting the possibility of turning such a method into a function, which is commonplace in Scala, but was impossible for methods whose return type was path dependent on the input type. Now we can do it:

def fooMethod(a: A): a.Foo = a.key
val fooFunction: (a: A) => a.Foo = fooMethod

Second most important function types feature, and I find this one personally very exciting, are implicit function types. There has been some confusion over this, so let me try to clear it up. Let’s take the following implicit function type:

type MyFunction[B] = implicit A => B

This *does not* have the semantics of “implicit conversion from A to B”. Think of it like a “function of type A => B, where A is implicit”. Like in other scenarios with implicits, this means that if implicit value of type A is found in the scope, it will be passed; if no implicit A is in scope, it needs to be passed explicitly or the compilation will fail.

So given some method

def foo(f: Foo)(implicit a: A): B = ???

we can rewrite that as:

type MyFunction = implicit A => B
def foo(f: Foo): MyFunction

This allows us to define foo as a *function value*. Without implicit function types, we can only define it as a method; right now in Scala 2, having implicit parameters in a method automatically means that it cannot be expressed as a function.

Implicit function types allow us some very handy refactoring. Unfortunately, I don’t have nearly enough space here to do full justice to implicit function types in terms of explaining their full usefulness. It would require a blogpost on its own, which I might even write one of these days. Several interesting use cases (e.g. removing boilerplate in the tagless final pattern, refactoring Dotty compiler context passing, some implementations of SQL queries etc.) have been presented in this very interesting talk by Olivier Blanvillain at ScalaDays in Berlin, so if at the time of reading this a video of it is on YT, I really recommend you to watch it.

 

Generic tuples

This one is still open at the time of writing this, but it’s happening for sure in Scala 3. Tuples are no longer going to be implemented via those awkward TupleN traits that (quite arbitrarily) end at Tuple22. Instead, they are going to be implemented similarly to how Lists are implemented, with their recursive-friendly head :: tail structure, which means that they are basically becoming shapeless HList.

Even though this means that the 22 limit is gone, that’s not the main advantage here (if you’re using a tuple with 22+ values, you should probably reconsider your domain design). Main improvement comes in the way we treat the tuples themselves, because nested tuples can now be treated as flattened;(a, b, c) will be exactly the same as(a, (b, (c, ()))). This allows for some nice generic programming similarly to what we can do with HLists, such as mapping over them with monomorphic or even polymorphic functions (the latter requires some shapeless trickery; check out these excellent blogposts by Miles Sabin).

 

Opaque types

Here’s a short explanation of a newtype for those who aren’t familiar with it: it’s similar to a type alias, but rather than being an alias just for the programmer, it’s also an alias for the compiler, meaning that the compiler actually differentiates the two. Let’s take the following type alias as an example:

type FirstName = String

This gives us the possibility of using “FirstName” as a type, 

which can make the code more understandable. However, nothing prevents us from actually passing a “normal” String wherever FirstName is required. Even worse, if we have another type alias, e.g. LastName = String, nothing prevents us from accidentally passing a LastName where FirstName is expected and vice versa, since from the compiler’s point of view they are all just Strings.

If we want to achieve the behaviour where such usage would be prevented by the compiler, we need to resort to value classes. This means writing something like this:

class FirstName(val underlying: String) extends AnyVal
class LastName(val underlying: String) extends AnyVal

This is a bit boilerplate-y and has a slight performance hit due to boxing/unboxing. Adding the extends AnyVal part takes care of some of that performance penalty, but not in all use cases. Plus, our code looks awful!

There’s a library that allows defining them in a slightly more elegant way:

@newtype case class FirstName(underlying: String)

but this still feels like the same old boilerplate with some new syntax. Also, these newtypes must be defined in an object or package object.

Scala 3 is introducing opaque types.

opaque type FirstName = String

People sometimes use the terms newtype and opaque type interchangeably. Others say that newtype is the one that wraps around the underlying type as we’ve seen earlier, while opaque types are defined “directly”. Others yet say that opaque types are a special kind of type alias that is created using a newtype. Don’t get confused by the terminology; concept is pretty much the same.

So yeah, opaque types give us exactly what we need. FirstName is a type on its own, and passing a value of type LastName where FirstName is expected won’t compile. There might be some resistance to introducing a new keyword, in which case the proposal is to use: new type FirstName = String. Either way, this will be a nice feature.

 

Type lambdas

We’re getting full language support for type lambdas without having to resort to ugly boilerplate or external libraries.

Let’s take a popular use case where F[_] is needed, but we want to pass a type constructor that takes two types (such as Map or Either), or in other words, we want to pass (﹡ → ﹡) → ﹡where ﹡→ ﹡is needed.

Instead of having to define a type lambda like this:

({ type T[A] = Map[Int, A] })#T
we will be able to simply say
[A] => Map[Int, A]

Type parameters of lambdas will support variances and bounds, for example

 

[+A, B <: C] => Whatever[A, B, C]

 

Erased parameters

There are situations where we only need some parameter(s) in the signature, e.g. for evidence in generalized type constraints, and they are never used in the body itself. Unnecessary code will still be generated for such parameters, which can be prevented by using the keyword “erased”.

For example,

def foo[S, T](s: S, t: T)(implicit ev: S =:= T)
could and should be used as
def foo[S, T](s: S, t: T)(implicit erased ev: S =:= T)

So whenever you have one or more parameters that are used only for type checking, using “erased” keyword will make your code more performant.

 

Enums

Yep, one of the clunkiest constructs in Scala gets a full redesign, which means replacing code like this:

object WeekDay extends Enumeration {
  type WeekDay = Value
  val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}

with code like this:

enum WeekDay {
  case Mon, Tue, Wed, Thu, Fri, Sat, Sun
}

They can be parameterized and can contain custom members. They will also have some handy methods already pre-defined.

Example:

enum Weekday(val index: Int) { 
  private def next(i: Int) = (i + 1) % 7
  private def prev(i: Int) = (i + 7) - 1 % 7
  def nextDay = Weekday.enumValue(next(index))
  def prevDay = Weekday.enumValue(prev(index))
  case Mon   extends Weekday(0)
  case Tue   extends Weekday(1)
  ...
}

Other

Here are some additional features and improvements that I find important as well, but they don’t really require separate paragraphs to be explained.

Multiversal equality:

Compiler will now match the types when comparing values and fail the compilation if there’s a mismatch, e.g. “foo” == 123. We can already model this via type classes (it’s actually done in several libraries), but with Scala 3 we’re getting the native language support.

Restricting implicit conversions:

Compiler will now require a language feature import not only when an implicit conversion is defined but also when it is applied.

Null safety:

This is based on the union types feature, which allows us to define the result type of scary operations that can throw a null pointer exception (useful for interoperability with Java) as Foo | null.

Extension clauses:

These will basically be a replacement for implicit classes, e.g. 
extension StringOps for String { … }, but I didn’t dig too deep into this yet. Take a look at this PR if you want to find out more.

Removing unneeded constructs:

…such as early initializers, auto-tupling, existential types using forSome, automatic insertion of (), infix operators with multiple parameters, etc.

 

Conclusion

Scala 3 is going to be a big change, no doubt about that. I just hope that the upside of getting all these new exciting goodies will not be met with the downside of difficult switch from 2 to 3, which might result in a split community (such as when Python moved to the same major release). Tools are being developed in order to make the transitions and cross builds as painless as possible. One interesting thing that’s coming up is TASTY (Typed Abstract Syntax Trees; I guess “Y” is just making the acronym catchy). These trees contain all the information present in a Scala program, such as syntactic structure, info about types, positions etc. Once serialised into a TAST, it will be possible to port the Scala program to a different version, which will allow easy cross builds and solving the problems of binary (in)compatibility.

But let’s not ruin the moment talking about real-life problems :) as far as the language itself is concerned, Scala 3 is bringing a lot of improvements and interesting new features to the table, and I’m looking forward to it.

Thanks for reading."

This article was written by Sinisa Louc and posted to Medium.com.

 

Are you looking forward to Scala 3? Let us know your thoughts!