14-Dec

Functional

A peek into Scala 3

7 min read

·

By Per Øyvind Kanestrøm

·

December 14, 2020

A new year is closing in and so is a new release from Scala. Release candidate 1 is expected to be found sometime this December!

Version two of the language was released in 2006 and has so far seen a total of thirteen larger minor releases. Throughout these changes the language has evolved to places not foreseen. Implicits that were first introduced for implicit conversions gave rise to type classes, built upon to create implicit classes for extension methods, and macros for metaprogramming that to this day is still locked behind experimental flags, though still considered an important part of the language. A key part of the coming version is to formalize these patterns and be more opinionated. But under the hood there is lots more going on!

A unique challenge for Scala has been the combination of subtyping and dependent types. The theoretical foundation has been on rocky grounds up until 2016 when DOT Calculus (Dependent Object Types) was presented^subtypingPath. With that came Dotty, a reference implementation language translating to DOT calculus, and now soon to be Scala 3. Though going deep into these details is not important for end users, it is interesting to note that we are talking about a complete rewriting of the compiler.

If migration issues are a worrying factor, then it should please you to know that most application code bases seem simple to port. To ease the process a lot of the bigger users facing changes are delayed to version 3.1. Scala 3 will also be able to use Scala 2.13.4 binaries and vice versa. Most of the difficulties will be in the ecosystem migrating base libraries, which is well on its way! The community builds^scala2CommunityBuilds has been an enormous success for Scala 2 and the one for Scala 3 is soon up to 50 community libraries ^scala3CommunityBuilds! Not bad for unreleased software. Documentation on the migration process is being worked on the by the Scala Center^migration.

New Features

@main
def hello = println("world")

Simplicity and more opinionated patterns are one of the bigger design goals. In the example above we have started a small program in two lines of code. Try it out yourself through Scastie^scastieHelloWorld. But lines of code are far from everything regarding simplicity! The error messages are improved, and the type system will do a much better job aiding you while keeping the code sound. One example of that is that the compiler will give suggestions of what to import when implicits are missing from the scope. In the example error below, we can see a detailed error message from trying to use an extension method from the fantastic Cats^cats library. Learning which imports to use in libraries which are heavy on implicit usage has always been a sore spot for beginners. Now we immediately get a helping hand to continue.

 List(1, 2, 3)
    .foldMap(number => Option.when(number > 1)(number - 1))
// [error] -- [E008] Not Found Error: Main.scala:365:5
// [error] 364 |  List(1, 2, 3)
// [error] 365 |    .foldMap(number => Option.when(number > 1)(number - 1))
// [error]     |  ^
// [error]     |value foldMap is not a member of List[Int], but could be made available as an extension method.
// [error]     |
// [error]     |One of the following imports might fix the problem:
// [error]     |
// [error]     |  import cats.Foldable.nonInheritedOps.toFoldableOps
// [error]     |  import cats.Foldable.ops.toAllFoldableOps
// [error]     |  import cats.Traverse.ops.toAllTraverseOps
// [error]     |  import cats.implicits.toFoldableOps
// [error]     |  import cats.syntax.all.toFoldableOps
// [error]     |  import cats.syntax.foldable.toFoldableOps
// [error]     |
// [error] one error found

All the examples can be run through Scastie and I can recommend trying them out. Note that they are an unprioritized random subset of all the new features. An up-to-date list of changes to the language can be found on the Dotty site^scala3Docs.

Enums

In earlier versions the de facto standard of enums have been to create a Java file with an enum, or create a sealed trait, or use some macro library. While sealed trait hierarchy do provide a working solution with exhaustive checking, it is far more general and requires a lot more boilerplate if one needs ordinality, hence the macro-based solutions.

enum Color(val rgb: Int) {
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)
}

Color.fromOrdinal(1) == Color.valueOf("Red")

// Or just

enum House {
  case House, Apartment
}

It is worth noting that enums are syntactic sugar that is translated to a sealed trait at compile time. Enums will therefore also support the modeling of ADT's (Abstract Data Types) and GADT's (Generalized ADT's).

A homemade implementation of the Option ADT can now be as follows:

enum Option[+T] {
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]
}

The new implicits: using/given and extension methods

One of the bigger sources of confusion have been implicits and all the contexts it is usable in. Need implicit conversion? Check. Type classes? Check. Extension methods? Check. Sending some contextual information? Check.

Extension Methods

Extension methods can shortly be explained as extending methods on types after they have been defined. This can be usable for extending the functionality of third-party libraries or for creating DSLs.

Let us say one wants to count the occurrences of a word in a list, say

val myList = "My cat knows how to not disturgbdds mejkljljlsdjflkds"

myList.countWord("cat")

The function countWords does not exist. Previously with the following in scope the code would have compile.

implicit class CountOps(sentence: String) {
  def countWord(word: String): Int =
    sentence.split(" ").count(_.toLowerCase == word.toLowerCase)
}

While this works perfectly fine, it is not easy for someone new to the language to understand what is going on. So now we can instead use the keyword extension, thus making the intention a lot clearer and easier to Google!

extension (s: String) def countWord(w: String): Int =
  s.split(" ").count(_.toLowerCase == w.toLowerCase)

Type classes

Type classes are, in short, a way to do ad hoc polymorphism. One typical example of a type class is the Show type class. In Java, every class extends from Object and on Object we find a toString method. Being available everywhere is practical, but does it make sense for everything? I am not going to jump into that debate, but what if we could define something that gave us a def show: String method on every type that we find necessary, and if not defined then give us a compile error? To have such a class of functionality defined ad hoc for a type is a type class.

In Scala 2 this could look like the following (if you are new to the language, do not ponder on this example for too long!):

// Our type class that is a function to run `show` on some unknown generic A
trait Show[A] {
  def show(value: A) : String
}

// Our instance for the String type
implicit val showString: Show[String] = new Show[String] {
   def show(in: String): String = in
}

// Our instance for the Int type
implicit val showInt: Show[Int] = new Show[Int] {
   def show(in: Int): String = in.toString
}

// To create extensions method capability
implicit class ShowOps[A](in: A)(implicit show: Show[A]) {
  def show: String = show.show(in)
}

val result: String = 1.show

@main
def run = println(result)

This works for its purpose, but there are a lot of concepts to grasp. If finding this in your codebase is the first introduction to these concepts, then good luck guessing what to google to understand it.

A rewrite of the same functionality to Scala 3 would use the new given keyword and extension methods.

trait Show[A] {
  extension (value: A) def show: String
}

given Show[String] {
  extension (value: String) def show: String =
    value
}

given Show[Int] {
  extension (value: Int) def show: String =
    value.toString
}

val result: String = 1.show

@main
def run = println(result)

One of the many small but significant differences to note is that there is no explicitly named reference to the given instance of the type class instances like showInt and showString in the Scala 2 example.

The keyword given is now used to define that something is implicitly available. To get a given value for some type then using is the new keyword:

import math.Numeric.Implicits._

def myFunc[A](in: A)(using Show[A], Numeric[A]) = {
  val abs = Numeric[A].abs(in)

  println(s"Absolute of ${in.show}: ${abs.show}")

  result
}

val result = myFunc(2.2) // Type is Double
// > 2.2

val result1 = myFunc(2) // Type is Int
// > 2

@main
def run = println(result)

For myFunc we have defined a function where we work on a type A that has an implementation of Show that is unnamed and defined numeric operations through a new type class, Numeric. The power of this is that within the function we have a well-defined set of things to do. If this had been from separate library, then we could just implement the given instances for our own types.

Show is not one of the conceptually important examples, but the use case for type classes is none the less a fantastic tool for many abstractions. Such examples can be to define specific functionality for types in generic code like myGenericFunction or modeling abstractions^applicatives.

Union types

To represent a coproduct of types.

type MyCoproduct = Int | String

Some of the possibilities this opens is to make type safe error handling simpler!

object SomeThirdPartyLibrary {
  case class BadState(code: Int)
  def getStuff: Either[BadState, Int] = Right(9001)
}

// My application:

import SomeThirdPartyLibrary._

case class OhNoYouDidNot(error: String)

def doTheThing(received: Int): Either[OhNoYouDidNot, String] =
  Either.cond(
    received >= 9000,
    "It's working!",
    OhNoYouDidNot(s"$received is not sufficient stuff")
  )

type MyErrors = OhNoYouDidNot | BadState

val operations: Either[MyErrors, String] =
   getStuff.flatMap(result => doTheThing(result + 1))

val message = operations match {
  case Right(result) =>
    result
  case Left(OhNoYouDidNot(thisThing)) =>
    s"You tried do that thing: $thisThing"
  case Left(BadState(code)) =>
    s"Unexpected error: $code"
}

@main
def run = println(message)

Previously this could be done by stacking Eithers or by making a sealed trait hierarchy. The downside of sealed traits is that the entire hierarchy must be defined in the same file upfront. Now, error types can be combined and built up anywhere!

Opaque types

Another mechanism to help with type safety in our applications is opaque types. They are type definitions where the enclosing type is only known within the same scope.

object MyMathLibrary {
  opaque type Percent = Double

  object Percent {
    def apply(in: Double): Percent = {
      assert(in >= 0 && in <= 100)
      in
    }

    extension (x: Percent) def * (y: Percent): Percent =
      ((x / 100) * (y / 100)) * 100
  }
}

import MyMathLibrary ._

val result = Percent(10) * Percent(40)

@main
def run = println(result)

// There are no ways to get on Santa's naughty list:

//Percent(10) * 400 // type error
//[error] 76 |    Percent(10) * 400 // type error
//[error]    |                  ^^^
//[error]    |                  Found:    (400 : Int)
//[error]    |                  Required: Main.TestNumeric.MyLib.Percent

And lots more

Other interesting themes to dive into are intersection types, the newly built-in type class derivation support, explicit null handling, export clauses, and a new macro system. While the details are still being hammered out, it is interesting to ponder where all of this will take the language. Eager to try it out ^scastieHelloWorld?

14-Dec