Scala 3 Highlights
What's new in Scala 3?
- Created, Nov 19, 2020. Latest update: July 2, 2021
- Dean Wampler
This is a concise summary of many of the changes in Scala 3. I used these notes for talks at The Chicago-Area Scala Enthusiasts (CASE), Nov. 19, 2020, Scala Love in the City, Feb. 13, 2021, and at the Philly Area Scala Enthusiasts (PHASE), April 21, 2021.
Some of the code examples are in my Scala 3 blog. Most are adapted from the Code examples for Programming Scala, Third Edition with a few “borrowed” from the Dotty documentation (Dotty is the project that evolved into Scala 3).
For More Information
- My book, Programming Scala, Third Edition:
- Book website
- Code examples (warning, not many comments!!)
- My Scala 3 blog
- Scala 3 home page which links to the thorough Scala documentation. See in particular the following:
General Comments
Some features are transitional to make upgrading easier. Scala 3.0 lets you mix older Scala 2 features with their Scala 3 replacements, but subsequent releases will start deprecating and eventually removing the older features. I’ll discuss examples as we go.
To get started, SBT 1.5+ supports Scala 3:
New Syntax
You can now use significant indentation (“braceless”), like Python or Haskell, rather than curly braces. You can also mix and match, or use a compiler flag to force one or the other (see below).
Note: Additional changes to this syntax are coming in Scala 3.3. (Blog Post)
Import and Export Statements
Look at these examples of import
statements:
Scala 3 replaces _
with *
as the wild card for imports, which is what most other languages use. When you alias a type, you now use the new as
keyword instead of =>
. Hence, =>
is now used solely for function literals.
What if you want to import a multiplication method named *
? Use back ticks:
import Matrix.`*`
Scala 3 also introduces the concept of exporting members of a type. See the blog post for details.
Types
Methods
Partial Functions
Match Expressions
But “Custom Controls” Don’t Work (yet…)
Create a custom loop
“control”:
There is an experimental compiler flag -language:experimental.fewerBraces
that enables this to work, but there are details to work out before it’s considered fully supported (Scala 3.1??).
New Control Syntax
There are also new options for control syntax, but whether or not you use them is controlled by compiler flags:
Even Less Use of new
We don’t need to use new
when creating instances of case classes, because the compiler generates a companion object with an necessary apply
method. For other types, like the abstract Seq
type, one or more custom apply
methods exist in a companion object, e.g., Seq(1,2,3)
. In Scala 2, we had to use new
for all other kinds of classes, especially those from Java libraries.
Scala 3 extends the case class mechanism to all concrete types, even those from Java and Scala 2 libraries. The compiler creates a synthetic object with an apply
method for each constructor. (Note: for companion objects, only the primary constructor gets an apply
method automatically).
Contextual Abstractions
We begin the migration aware from the implicit mechanism to constructs that more clearly indicate the intent.
Extension Methods Instead of Implicit Conversions.
Remember the ArrowAssoc
implicit conversion??
It’s much simpler and more direct to use an extension method. The following example shows how to define a ~>
method on any type A
. The @targetName
annotation defines the name generated in byte code for the method (but this is only visible to other languages, like Java, not Scala code!).
Extension methods are part of a new syntax for type classes, which I’ll cover in a moment.
NOTE: If you also defined an
arrow2
method, it would collide with the target name given for~>
.
There are still cases where implicit conversions are useful, e.g., allow users to specify Double
values that are converted to domain types, like Dollars
and Percentage
:
Note the new given
syntax. This replaces implicit val/def
, in general.
Note the name that is synthesized for the first given instance if you don’t explicitly provide a name, given_Conversion_Double_Dollars
.
The second given instance is named d2P
. The declaration looks similar to a typical val
declaration.
By the way, the first definition is shorthand for this:
given Conversion[Double,Dollars] with
def apply(d: Double): Dollars = Dollars(d)
Even when a given is anonymous, you can use summon[Conversion[Double,Dollars]]
to bind to it. The new method summon
is identical to implicitly
; a new name for a newly-branded concept:
scala> summon[Conversion[Double,Dollars]]
val 0: Conversion[Double, Dollars] = <function1>
Maybe you already noticed that Conversion
looks shockingly similar to A => B
.
New Type Classes
The syntax combines traits (to define the abstraction), regular and extension methods, and given instances (for type class instances):
Notice which members are extensions and which ones aren’t! The extension methods will be instance members and the others will be the equivalent of companion object members; we only need one unit
per type T
. (The fact they are split across two different types is incidental to how I defined them. All could be defined in the same type.)
Create two monoid instances:
Try them out:
We can actual define the monoid instance for all T
for which Numeric[T]
exists:
Now we see our first example of a using clause, the successor to an implicit parameter list.
Finally, like the Conversion[Double,Dollars]
example above, given
s will often be declared anonymous:
Note that we use summon
to retrieve the Numeric[T]
object. Here, the anonymous given
is less convenient than a named given
when we need the unit
value.
The definition of the anonymous given
may be a little hard to parse. Go back to the previous named definition and just remove the name!
Using Clauses
We just saw a using clause. They can be anonymous, too. Here’s an (unnecessary ;) wrapper around Seq
for sorting them:
I passed the implicit/given
values explicitly to Seq.sortBy
for illustration purposes, but of course I could have passed them implicitly (usingly?). The term context parameters (or arguments) is used for these parameters.
Note that the using
keyword is now required when you pass an explicit argument (although optional in the 3.0 release, to ease the transition). Requiring the keyword when you pass an explicit argument disambiguates which clauses are using clauses and which aren’t. This permits more flexible definitions like the following:
By-Name Context Parameters
In general, by-name parameters are great for deferred evaluation, such as passing expressions that should be evaluated inside a method, not before calling the method. This works for context parameters, too, as in this sketch of a database access API:
Context Functions
Context functions are functions with context parameters only. Scala 3 introduces a new context function type for them, indicated by ?=> T
. Distinguishing context functions from regular functions is useful because of how they are invoked.
Consider this example that wraps Future
s:
Executable[T]
is a type alias for a context function that takes an ExecutionContext
and returns a T
.
Note the syntax for FutureCF.apply
compared to Future.apply
, shown in the comment. It’s a simpler syntax than having a using clause, but it can be a bit confusing that a Future(...)
is returned, not an Executable
.
Notice that we use FutureCF.apply
just like Future.apply
, either using the given
global
value or providing it explicitly. So, how do we get the Exetuable[T]
returned when the body of FutureCF.apply
is just Future(...)
?
Executable(Future(sleepN(1.second)))
is returned,- which is the same as
(given ExecutionContext) ?=> Future(sleepN(1.sec ond))
(from the type alias forExecutable
), - which is invoked to produce
Future(sleepN(1.second))(using global)
, - which is invoked to return the
Future
.
You can also define methods that take context functions as arguments. Study this Dotty documentation for more examples and an explanation of how the compiler handles these definitions.
Given Imports
To allow use of *
for imports, but not pull in all givens when you don’t want them:
In Scala 3.0, *
will still import everything, for backwards compatibility, but Scala 3.1 will begin transitioning to this behavior.
NOTE: “non-givens” should be called takes IMHO… If you grew up with the King James Bible (1611) in your Baptist church like I did, they would be
giveth
andtaketh
…
Alias Givens
Consider the following definitions, which sort of look similar to our previous definitions of Monoids, but in fact they are different.
Note the definitions the REPL prints for the two givens, a method for the given with a type parameter T
and a lazy val
for the StringMonoid
.
That explains the “Initializing …” messages. Every time we use the <+>
method for numeric values, the method NumericMonoid2[T]
is called, instantiating a new monoid instance. For StringMonoid
, this only happens once, like for other lazy values.
Type Class Derivation
For some type classes, why do we have to write the boilerplate for givens. Can the compiler infer the definition for us? That’s the goal of type class derivation, which Scala 3 supports.
There is a built-in, derivable type class called CanEqual
, which is used with a language feature called strict equality to make equality checking more restrictive, causing the compiler to reject obvious false comparisons between objects of different types that can never be equal. Recall that Scala 2 follows Java conventions that equals is declared def equals(other: AnyRef): Boolean
(for reference types).
In the following example, we enable the language feature just for this file, then declare an enum
for Tree
s, which derives CanEqual
. Finally, we check what’s allowed. Note that attempting to compare instances of Tree[Int]
with Tree[String]
is rejected at compile time:
See the Dotty documentation for details about how CanEqual
is implemented and how to implement your own derivable type classes. (The new enum
syntax is discussed below.)
Finally, for this particular example, recall that normally we’re allowed to compare any AnyRef
to another AnyRef
, even if they will never be equal. This is universal equality. The CanEqual
type class supports multiversal equality, where types are grouped in such a way that comparison of instances is only allowed for types within the same group. That doesn’t necessarily mean the same exact type. In the example, we can compare a Branch[T]
and a Leaf[T]
for the same T
, but not compare instances for different T
s.
Infix Operator Notation
Because people abuse operator notation, Scala is migrating towards disallowing it, by default, unless:
- The method name uses only “operator characters”.
- The method is declared
infix
. - The argument is wrapped in curly braces.
- Back ticks are used:
In our previous example:
scala> "2" combine ("3" combine "4")
|
1 |"2" combine ("3" combine "4")
| ^^^^^^^
|Alphanumeric method combine is not declared `infix`; it should not be used as infix operator.
|The operation can be rewritten automatically to `combine` under -deprecation -rewrite.
|Or rewrite to method syntax .combine(...) manually.
1 |"2" combine ("3" combine "4")
| ^^^^^^^
| (same error message)
scala> "2" combine { "3" combine { "4" } }
val res0: String = 234
scala> "2" `combine` ("3" `combine` "4")
val res1: String = 234
Or, declare combine
with infix
, then you can use "2" combine ("3" combine "4")
:
Note: I had to redefine the previous monoid instances with this new definition to add
infix
to each concrete definition ofcombine
.
Now combine
can be used with infix notation as an alternative to <+>
:
scala> "2" combine ("3" combine "4")
val res2: String = 234
Easier Enums
I can never remember the Scala 2 syntax for enums. Now I have an even easier syntax to forget!
Adapted from Dotty docs:
NOTE: As commented by Seth Tisue during the CASE meeting, Scala 3 uses the Scala 2 library unchanged, so types like
Option
won’t be changed to enums until some future release.
Type Madness!! (Or Sanity…)
Opaque Type Aliases
Opaque type aliases have advantages and disadvantages compared to value classes. Example adapted from the Dotty docs:
In action:
Value classes still have a few advantages. They are real classes, so you can customize the equals
and toString
methods for them and you can pattern match on them (although this causes boxing to be required). Opaque type aliases don’t provide these benefits, but they prevent occurrences of boxing. Finally, the JDK will eventually have its own form of value classes (the Valhalla project), in which case a Scala representation will be desirable.
Open Classes
No more ad-hoc extensions of concrete types (unless you want ‘em):
(Wouldn’t make sense for abstract classes and traits, which must be extended to become concrete…)
What if a type isn’t open, but you want to subclass it in a test to stub methods (i.e., make a test double)? Use import scala.language.adhocExtensions
in the test file. This is the advantage over declaring the type final
, which provides no mechanism for this sort of “exceptional” extension.
Intersection and Union Types
blog post (Note: I used different examples in the post than the ones below. The post also goes into more details about algebraic properties of these types.)
Intersection Types
Intersection types work much like with
for trait mixins:
trait Resettable:
override def toString:String = "Resettable:"+super.toString
def reset(): Unit
trait Growable[T]:
override def toString:String = "Growable:"+super.toString
def add(t: T): Unit
def f(x: Resettable & Growable[String]): String =
x.reset()
x.add("first")
x.add("second")
x.toString
Note how the argument x
for f
is declared.
I find it a little confusing, but you have to actually instantiate these types using with
:
case class RG(var str: String = "") extends Resettable with Growable[String]:
override def toString:String = s"RG(str=$str):"+super.toString
def reset(): Unit = str = ""
def add(s: String): Unit = str = str + s
case class GR(var str: String = "") extends Growable[String] with Resettable:
override def toString:String = s"GR(str=$str):"+super.toString
def reset(): Unit = str = ""
def add(s: String): Unit = str = str + s
I declared two types, one with Resettable & Growable[String]
and the other with Growable[String] & Resettable
. They are considered the same type by f
. In other words, they commute, just like the intersection operation in set theory. In contrast, Scala 2 treated Resettable with Growable[String]
and Growable[String] with Resettable
as different types.
Let’s try them!
scala> val rg = new RG
| val gr = new GR
|
val rg: RG = RG(str=):Growable:Resettable:rs$line$16$RG@173b3581
val gr: GR = GR(str=):Resettable:Growable:rs$line$16$GR@367164f5
scala> f(rg) // Both can be passed to `f`, showing commutativity.
| f(gr) // But toString shows different ordering of the "supers"!
|
val res0: String = RG(str=firstsecond):Growable:Resettable:rs$line$16$RG@697f3db1
val res1: String = GR(str=firstsecond):Resettable:Growable:rs$line$16$GR@b3cd3f1e
Note that we got Growable:Resettable
vs. Resettable:Growable
.
(The :rs$line$...
comes from calling super.toString
on the object wrapping the code in the REPL! Just ignore it…)
Linearization is still used to decide which of an overridden method gets called when you use super.method(...)
. In this example, super.toString
returns different results for Resettable & Growable[String]
vs. Growable[String] & Resettable
, even though those two types are considered equivalent from the type-checking perspective! Note which trait’s toString
got called first for each case. (Hint: linearization is basically right to left ordering.)
WARNING: While
A & B
andB & A
are considered equivalent types, the behaviors of overridden methods may be different, due to linearization.
Union Types
Union types could replace Either[A,B]
, but they aren’t limited to two nested types. Consider this pseudo DB query example:
case class User(name: String, password: String)
def getUser(id: String, dbc: DBConnection): String | User | Seq[User] =
try
val results = dbc.query(s"SELECT * FROM users WHERE id = $id")
results.size match
case 0 => s"No records found for id = $id"
case 1 => results.head.as[User]
case _ => results.map(_.as[User])
catch
case dbe: DBException => dbe.getMessage
getUser("1234", myDBConnection) match
case message: String => println(s"ERROR: $message")
case User(name, password) => println("Hello user: $name")
case seq: Seq[User] => println ("Hello users: $seq")
Note how pattern matching is necessary to determine what was returned from getUser
. Compared to Either
, you give up the useful operations like map
, flatMap
, etc. as alternatives to pattern matching like this.
Speaking of match expressions…
Match Types
This can be a bit “quirky”, but it’s a cool feature. (Example adapted from the Dotty docs):
type Elem[X] = X match
case String => Char
case Array[t] => t
case IterableOnce[t] => t
case ? => X
val char: Elem[String] = 'c'
val doub: Elem[List[Double]] = 1.0
val tupl: Elem[Option[(Int,Double)]] = (1, 2.0)
val bad1: Elem[List[Double]] = "1.0" // ERROR
val bad2: Elem[List[Double]] = (1.0, 2.0) // ERROR
summon[Elem[String] =:= Char] // ...: Char =:= Char = generalized constraint
summon[Elem[List[Int]] =:= Int]
summon[Elem[Nil.type] =:= Nothing]
summon[Elem[Array[Float]] =:= Float]
summon[Elem[Option[String]] =:= String]
summon[Elem[Some[String]] =:= String]
summon[Elem[None.type] =:= Nothing]
summon[Elem[Float] =:= Float]
summon[Elem[Option[List[Long]]] =:= Long] // ERROR
The last one fails because our match type doesn’t handle nesting beyond one level. This is possible; the type can be recursive!
Matchable
Scala 3 introduces a new trait called scala.Matchable
. It is used to fix some loopholes in pattern matching. It has a lot of implications for your code. See my blog post for details.
Type Lambdas
Type lambdas are the type-level analog of “value lambdas”, i.e., functions. Scala 3 intoduces a syntax similar to function syntax, e.g., type Work = [T] =>> Either[String,T]
. Note the similarity of =>>
to function literal “arrows” =>
.
This example suggests one important use for type lambdas; when you have a type with two type parameters that you want to use in a context where a single type parameter is expected, but one of the type parameters can be fixed. The blog post provides a more extensive example.
Metaprogramming
The metaprogramming system is completely new in Scala 3. See my blog posts on inline
and macros for more details. See also this separate overview of inline
. (I have more posts planned about Scala 3 metaprogramming.)
Migration
The book’s code examples use the flag -source future
to force deprecation warnings for older constructs. The default, -source 3.0
, is more forgiving.
Flags to control syntax preferences:
-noindent
: Require classical {…} syntax, indentation is not significant.-new-syntax
: Requirethen
in conditional expressions.-old-syntax
: Require(...)
around conditions.
Flags to help migration:
-language:Scala2
: Compile Scala 2 code, highlight what needs updating.-migration
: Emit warning and location for migration issues from Scala 2.-rewrite
: Attempt to fix code automatically.-indent
: Use with-migration
and-rewrite
to transform code into braceless notation.
Other Things of Note…
- Traits can have constructor parameter lists, like classes.
- Kind polymorphism: generalize over
A
,F[A]
,G[A,B]
, … - Dependent function types - We’ve had dependent method types. Now functions can be dependently typed. See this blog post.
- Polymorphic function types - We’ve had polymorphic methods, e.g.,
def size[T](seq: Seq[T])Int = seq.size
. Now functions can be polymorphic. See this blog post. - New types like
Tuple
,EmptyTuple
(and tuple operations). - Explicit
null
s:def callJavaMethod(...): String | Null
. - Safe initialization.
@main
methods that can replaceobject Foo { def main(args: Array[String]):Unit = ??? }
boilerplate (although parsing of arguments is primitive in the first release).- No more 22-arity limits on tuples and functions.
- Transparent traits (i.e., they don’t show up in type inference).
- … and lots of other refinements.