Scala 3 comes with many amazing new features. This article attempts to explain the most notable ones, so it is by no means comprehensive. It is, however, a very good introduction to these concepts for beginner to intermediate-level Scala programmers.
At the time of this writing, Scala 3 isn’t actually officially released. However, all the features that would be in Scala 3 are available in Dotty, so we’ll be using it instead.
The examples in this article are on GitHub, and in order to run them, you can clone the repository and run sbt console
:
If you use Visual Studio Code, and you have it on the
PATH
(i.e. thecode
command is available globally,) you can runsbt launchIDE
to get a better editing experience.
Intersection Types
Consider the following definitions:
If we think of types in terms of sets, a type would be a collection of elements that satisfy certain properties. For an element to be a member of type A
, it has to have the property a
of type String
. The same goes for elements of type B
.
Since types are just sets, what would the intersection of A
and B
be? Well, it’s the set of elements that satisfy the properties of both A
and B
. We can denote such a type as A & B
(the intersection type of A
and B
,) and we can use it to specify data types such as the type of the argument c
in:
We can create an instance of A & B
by creating a subtype of both A
and B
:
Which can be passed to f
:
Intersection types are actually the equivalent of compound types, so both A & B
and A with B
are the same type. However, the with
syntax will be deprecated in future versions of Scala.
Union Types
In Scala, we can express that a value can be either of type A
or of type B
as Either[A, B]
. Consider this example:
The Either
type has two variants: Left
, and Right
. In the case of E
, Left
represents the variant that carries a value of type A
, while Right
carries a value of type B
(notice the order of the type arguments passed to Either
.)
Let’s say that we want to define a function g
of E
. In order to deal safely with values of E
, we would need to do some pattern matching:
This is great! We can safely express values that belong to one of two types. But there are a few problems with Either
:
- It’s not commutative (
Either[A, B]
is notEither[B, A]
) - It sucks for working with values that can be of three or more types (I mean,
Either[A, Either[B, Either[Float, Boolean]]]
? This is a nightmare!)
While still thinking of types as sets, let’s think of Either[A, B]
as the union of the sets A
and B
(the set containing all elements of both A
and B
.) This union can be expressed as A | B
:
Union types solve the two problems listed above, and they’re actually more pleasant to look at and deal with. Consider the definition of h
:
Which can be applied with simple arguments (no need to wrap in Left
or Right
.) For example:
Enums
Enums are definitions of types’ values by name. They’re useful when a value of a particular type can be one of a well-defined finite set of elements. For example, the definition of WeekDay
:
Enum values can be parameterized in order to implement algebraic data types (products, to be specific.) In Scala 2.x, we encode ADTs as case classes (or tuples,) and sealed traits. Let’s consider a definition of logical expressions:
Now we can evaluate the expression:
Which is just true ∧ ¬false ∨ true
. Notice how we’ve had to define a case class for each variant of VerboseLogicalExpression
, and made it extend the trait. We also did the same with Factor
just to get the behavior of ConstFactor | NotFactor
.
Using enums, we can define logical expressions as:
Much cleaner! Note that using the apply
method of an enum’s variant would return a value of the enum type, not the specific case type. We can use new
to use the constructor of the specific type. So we would define a value of type LogicalExpression.Expr
as:
Givens
Givens are definitions that can be used implicitly. In many ways, they are a more refined version of Scala 2.x’s implicits. Consider this example:
Here, we define a given instance of an Add[C]
. This is a more straightforward way of implementing typeclasses.
Givens do not need to have names, since their type is all that matters in most cases.
We can define functions that have given parameters to summon any defined given instance:
Similarly, we can define given value aliases:
Implicit conversions are replaced with the definition of a given instance of Conversion[T, U]
. For example, we can define an implicit conversion from String
to A
as:
Using wildcard syntax in imports will not import any given definitions. Instead, you must use .given
syntax, or import the given definitions by name. See here for more information about imports.
Extension Methods
Scala 3 offers a really simple way to add new methods to existing types. Consider the following example:
This definition (when in scope,) adds the +
method to any type T
for which a given instance of Add[T]
is defined, such as the type C
.
In 2.x, you would have to define an implicit class with the extension method defined on it to achieve this. This is a much simpler approach.
Conclusion
There still are many features not covered here: typeclass derivation, match types, and an entirely new macro system, just to name a few. This article would need to be much longer than this in order to cover all of them. Luckily, the documentation offers a great introduction to these concepts, even though it might be lacking in some regards, but that’ll surely get better with the official release of Scala 3.