# A Guide to Scala 3

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`

:

`git clone https://github.com/Tabzz98/guide-to-scala3-examples.git`

cd guide-to-scala3-examples/

sbt console

If you use Visual Studio Code, and you have it on the

`PATH`

(i.e. the`code`

command is available globally,) you can run`sbt launchIDE`

to get a better editing experience.

# Intersection Types

Consider the following definitions:

traitA {

vala:String}traitB {

valb:Int}

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:

**def** f(c: A & B) **=** c.a **+** " & " **+** c.b

We can create an instance of `A & B`

by creating a subtype of both `A`

and `B`

:

**case** **class** C(a: **String**, b: **Int**)

**extends** A **with** B

Which can be passed to `f`

:

`scala> f(C("Some String", 42))`

val res0: String = Some String & 42

Intersection types are actually the equivalent of compound types, so both `A & B`

and `A `

are the same type. However, the **with** B

syntax will be deprecated in future versions of Scala.**with**

# 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:

vala=newA {vala="Some String" }valb=newB {valb=42 }typeE=Either[A, B]vall:E=Left(a)valr:E=Right(b)

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:

**def** g(e: E)**:** **String** **=** e **match** {

**case** Left(a) **=>** **s**"String value a: ${a.a}"

**case** Right(b) **=>** **s**"Int value b: ${b.b}"

}

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 not`Either[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`

:

**type** U **=** 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`

:

**def** h(v: A **|** **Double** **|** B **|** **Boolean**)**:** **String** **=** v **match** {

**case** a: A **=>** **s**"String value a: ${a.a}"

**case** d: **Double** **=>** **s**"Double value d: $d"

**case** b: B **=>** **s**"Int value b: ${b.b}"

**case** bool: **Boolean** **=>** **s**"Boolean value bool: $bool"

}

Which can be applied with simple arguments (no need to wrap in `Left`

or `Right`

.) For example:

scala> h(true)

val res0: String = Boolean value bool: truescala> h(a)

val res1: String = String value a: Some Stringscala> h(42.22)

val res2: String = Double value d: 42.22

# 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** WeekDay {

**case** Sunday, Monday, Tuesday, Wednesday,

Thursday, Friday, Saturday

}

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:

sealedtraitVerboseLogicalExpression {

importVerboseLogicalExpression._

defeval:Boolean=thismatch{

caseConstFactor(c)=>c

aseNotFactor(c)=>!c

caseTerm(l, Some(r))=>l.eval&&r.eval

caseTerm(l, None)=>l.eval

caseExpr(l, Some(r))=>l.eval||r.eval

caseExpr(l, None)=>l.eval

}

}objectVerboseLogicalExpression {

sealed traitFactorextendsVerboseLogicalExpression

caseclassConstFactor(value: Boolean)extendsFactor

caseclassNotFactor(value: Boolean)extendsFactor

caseclassTerm(left: Factor, right: Option[Factor])

extendsVerboseLogicalExpression

caseclassExpr(left: Term, right: Option[Term])

extendsVerboseLogicalExpression

}

Now we can evaluate the expression:

scala> import VerboseLogicalExpression._scala> Expr(Term(ConstFactor(true), Some(NotFactor(false))), Some(Term(ConstFactor(false), None))).eval

val res0: Boolean = true

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:

enumLogicalExpression {

caseConstFactor(value: Boolean)

caseNotFactor(value: Boolean)

caseTerm(

left: ConstFactor|NotFactor,

right: Option[ConstFactor|NotFactor]

)

caseExpr(left: Term, right: Option[Term])defeval:Boolean=thismatch{

caseConstFactor(c)=>c

caseNotFactor(c)=>!c

caseTerm(l, Some(r))=>l.eval&&r.eval

caseTerm(l, None)=>l.eval

caseExpr(l, Some(r))=>l.eval||r.eval

caseExpr(l, None)=>l.eval

}

}

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:

importLogicalExpression._valexpr=newExpr(

newTerm(newConstFactor(true), Some(newNotFactor(false))),

Some(newTerm(newConstFactor(false), None))

)

# 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:

traitAdd[T] {

defadd(x: T, y: T):T

}givenAdd[C] {

defadd(x: C, y: C)=C(x.a+y.a, x.b+y.b)

}

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:

defzipAdd[T](xs: List[T], ys: List[T])(givena: Add[T]):List[T]=xs.zip(ys).map(a.add)scala> zipAdd(List(C("Welcome ", 18), C("Scala ", 0)),

| List(C("to", 22), C("3", 2)))

val res0: List[C] = List(C(Welcome to,40), C(Scala 3,2))

Similarly, we can define given value aliases:

givenInt=42defaddMagicNumber(x: Int)(givenmagicNumber: Int)=x+magicNumberscala> addMagicNumber(2)

val res0: Int = 44

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:

givenConversion[String, A] {

defapply(s: String)=newA {vala=s }

}scala> h(a) // a is converted from A to String

val res0: String = String value a: Some Stringscala> :type h

A | Double | B | Boolean => String

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:

**def** (x: T) **+**[T] (y: T)(**given** a: Add[T]) **=** a.add(x, y)

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`

.

`scala> C("Yay ", 2) + C("Simplicity!", 40)`

val res0: C = C(Yay Simplicity!,42)

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.

I hope this article was insightful for you. Consider following Heavenly-x on Twitter and here on Medium to get notified of any new content.