Scala - DOT and Dotty



Scala 3 uses Dotty which is a new compiler for Scala. Dotty has many language features and improvements.

Introduction of DOT and Dotty

The goal of the Scala programming language is to combine three elements:

  • Types
  • Functions
  • Objects

Other languages focused on a part of these elements. For example, Lisp (functions and objects). Java (types and objects) Haskell and ML focused on functions and types. However, Scala stands out as the first language which brings together all three elements.

Scala 3 moves forward in unlocking the combination of object-oriented programming (OOP) and functional programming (FP) at the typed level. Fusion of OOP and FP is built on the foundation of DOT Calculus (DOT = OOP + FP).

DOT stands for Dependent Object Types which is the foundation of Scala 3. The concept behind DOT is core of Scala and advances with precise and robust foundations. Dotty serves as the compiler for building Scala 3 using the DOT Calculus. DOT is type-calculus used to prove that Dotty's language rules and its types are correct. DOT formally checks if the language rules are correct. Compiler research team is working on this to really understand the core and solid basis of Scala 3's type system.

DOT programming language has a type system. It is a small language but it is expressive. DOT is a simpler version of Scala. It can represent Scala features. All Scala code simplifies to DOT calculus. Programming languages Scala 2 and 3 are almost the same except for a few differences. The difference is that the Scala 3 compiler uses Dotty. Binary names of Scala 2 file and Scala 3 file are as `scalac` and `dotc` respectively.

Scala 3 has removed some useless features so it is smaller than Scala 2 language. However, Scala 3 has included new constructs for more usability, simplicity and consistency. Note that Scala 2 codes also work on Scala 3.

Scala Type System

Scala 3 has a simple and consistent type system. Here, we have discussed some changes in its type system.

1. Existential Types

From Scala 3, existential types and type projections have been removed. These were unnecessary so removed for simpler type systems and other constructs. However, projection on concrete type is supported on Scala.

2. Intersection and Union Types

Scala 3 has replaced compound types from Intersection types and Union Types. So, the subtype hierarchy of Scala has become a lattice. So, the compiler can find least lower bounds and upper bounds easily. It is easy for compilers to infer data types.

For example,

def fetchData(url: String): Data | String =
  try
    // logic to fetch and process data from the given URL
    processData(url)
  catch
    case _ => "Error fetching data"

Unions are duals of Intersection types. Union type (A|B) has all values of type A and Type B. Intersection types have all those members and properties that are common in both the types (A and B). It has the property of commutative. It means that A|B is same as B|A, i.e., both are equal. To decide if A|B is A or B, you can use pattern matching for this verification.

Example of intersection types:

trait Log:
  def logMessage: String

trait Auditable:
  def auditAction: String

type LoggableResource = Log & Auditable

def performAudit(loggable: LoggableResource) =
  println(s"Auditing action: ${loggable.auditAction}")
  println(s"Logging message: ${loggable.logMessage}")

object myResource extends Log, Auditable:
  override def logMessage = "Log entry for resource"
  override def auditAction = "Audit resource action"

performAudit(myResource)

To represent common values of type A and type B, you use A&B. It has all members and properties of type A and type B. This property is also commutative, i.e., A&B is the same as B&A. But compound type is not commutative.

3. Type Lambda

Type Lambda comes with a cool syntax: [X] =>> F[X]. It is a higher-kinded type using type parameter X which creates type T often referring to X.

These are functions that go from one type to another:

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

trait OptionInstance extends Functor[Option] {
  override def map[A, B](fa: Option[A])(f: A => B): Option[B] = ???
}

The old way of emulation of type lambda in Scala 2 was bad. It used a general type projection operator #, which is now removed.

trait OptionInstance extends Functor[({ type T[X] = Option[X] })#T] {
  override def pure[A](x: A) = ???
  override def flatMap[A, B](fa: Option[A])(f: A => Option[B]) = ???
}

Traits

Until now in Scala 2, we did not pass parameters to a trait. To resolve this, a workaround was to turn the trait into an abstract class and provide its parameters in the subclass. This solution is fine unless we face issues with early initialization. The issue is that the abstract class parameters initialize after the subclass constructor evaluates. In Scala 3, no such problems arise because we can give parameters to traits.

For example, in Scala 3:

trait Animal(val sound: String)
class Cat extends Animal("Meow")
class Dog extends Animal("Woof")

Like classes, Traits can have parameters in Scala 3. These get evaluated before the trait initializes.

Enums

Writing enumerations was awkward in Scala before version 3. Also, creating basic ADT involved a lot of unnecessary repetitive code. So, it lacked expressiveness. Scala 3 introduces the 'enum' keyword for writing enumerations and ADTs easily. This one feature supports both enumeration and ADTs.

Using 'enum,' we can create a type with a set of named values. Let’s create a data type named "Color" with a fixed set of values:

enum Fruit:
  case Apple, Banana, Orange

To align our enum with Java enums, we extend java.lang.Enum.

enum Fruit extends java.util.Enum[Fruit]:
  case Apple, Banana, Orange

They can have parameters:

enum Fruit(val color: String):
  case Apple  extends Fruit("Red")
  case Banana extends Fruit("Yellow")
  case Orange extends Fruit("Orange")

With enums, you can also write very expressive ADTs.

enum Result[+T, +E]:
  case Success(value: T)
  case Failure(error: E)

Implicits

Scala 2 implicits had many confusing aspects. So, it causes a lot of head-scratching. These are hard to understand, error-prone, misused and overused. There can be some confusion due to its complexity. But some issues are avoidable and just plain annoying.

In Scala 2, implicits serve various purposes. Key uses include:

  • Offering Contextual Environment
  • Creating Type Class Instances
  • Implementing Extension Methods
  • Applying Implicit Conversions

In Scala 2, implicits focus more on the mechanism than the intent. Scala 3 focuses on intent, introducing new keywords like given and using. Scala 2 implicits will stay available for a while due to compatibility concerns.

1. Providing Contextual Environment

The context includes outside information/parameters implicitly understood by our program. Scala 2 uses implicit parameters to pass contextual information into programs. These are basic ways to abstract over context.

For example, if a program needs ExecutionContext, you can create implicit parameter for it:

import scala.concurrent._
import scala.concurrent.duration._

implicit val customExecutionContext: scala.concurrent.ExecutionContext =
  ExecutionContext.global

def cube(i: Int)(implicit val ec: ExecutionContext): Future[Int] =
  Future(i * i * i)

In Scala 3, you can pass a contextual environment by passing implicitly. The given keywords provide an instance of that contextual type. For example,

import scala.concurrent.ExecutionContext
import scala.concurrent.Await
import scala.concurrent.duration._

given ExecutionContext =
  ExecutionContext.global

import scala.concurrent.Future
def cube(i: Int)(using ec: ExecutionContext): Future[Int] = {
  Future(i * i * i)
}

2. Writing Type Class Instances

In Scala 2, a key use of implicits is creating instances for type classes. For example,

trait Comparator[T] {
  def compare(x: T, y: T): Int
}

implicit stringComparator: Comparator[String] = new Comparator[String] {
  override def compare(x: String, y: String): Int =
    x.compareTo(y)
}

Scala 3 introduces a special keyword for writing a type class instance.

trait Comparator[T]:
  def compare(x: T, y: T): Int

given Comparator[String] with
  override def compare(x: String, y: String): Int =
    x.compareTo(y)

3. Extension Methods

Creating extension methods in Scala 2 contains some repetitive code. To extend type in Scala 2, we create a wrapper class. Then use implicit function to wrap type into extended class.

For example,

import scala.language.implicitConversions

class EnhancedDouble(d: Double) {
  def cube: Double = d * d * d
}

object EnhancedDouble {
  implicit def enhanceDouble(d: Double): EnhancedDouble = new EnhancedDouble(d)
}

Scala 3 introduces 'extension' keywords for writing extension methods. It is simple and concise syntax. For example,

extension (d: Double) def cube: Double = d * d * d

4. Implicit Conversions

In Scala 2, implicit conversions could cause unexpected bugs and require extra caution. In Scala 2, here is how to write an implicit conversion turning a String into Int:

import scala.language.implicitConversions

implicit def stringToDouble(str: String): Double = str.toDouble

def cube(d: Double): Double = d * d * d

In Scala 3, for implicit conversion from A to B, we need to provide a Conversion[A, B] instance.

import scala.language.implicitConversions

given Conversion[String, Double] = _.toDouble

def cube(d: Double): Double = d * d * d

Misusing implicit conversions in Scala 3 is hard. They eliminate unexpected behaviors. This method has fewer errors compared to Scala 2.

Advertisements