Skip to content

Instantly share code, notes, and snippets.

@ferhtaydn
Created June 20, 2017 10:59
Show Gist options
  • Save ferhtaydn/13e2bffa6480fcba0a8bf7e600a9f5aa to your computer and use it in GitHub Desktop.
Save ferhtaydn/13e2bffa6480fcba0a8bf7e600a9f5aa to your computer and use it in GitHub Desktop.
what's new in dotty?

Dotty

At the Scaladays Copenhagen Opening Keynote, Martin Odersky explains why Dotty has been needed, what is different in Dotty and what kind of problems will be solved when it becomes stable. The other important talk is Dimitry Petrashko's Dotty is coming, especially the Q&A session gives very valuable explanations to some of new dotty features.

The core dotty team is working hard to create a more powerful and stable, simpler, and more reasonable compiler (dotc) for Scala. Not only a new compiler, but also many new features have been added, many current features have already been dropped or changed. The dotty team cares community engagement seriously and waits feedback while many things are in progress or even at the decision stage. In order to feed the community about what's going on, dotty provides a very clean docs page in which all the details of that features and changes are explained. Even if dotty is very premature now, you can create a new empty sbt project based on dotty by using dotty.g8 template. There is also a dotty-example-project repo which contains almost all new dotty features with compiling and running code examples.

After watching Odersky's talk, I checked the dotty docs and example repos. I noticed that dotty-example-project repo contains only a simple Union Types example. I started to try existing examples from the docs to see how they are working and how much I can feel comfortable with new features. Then, I tried more things by extending current examples and adding new ones. All of these examples are now in the dotty-example-project repo, after my two pull-requests (1 and 2) are merged.

In this post, I would like to go over each dotty feature with examples by using above mentioned resources. I will get help from Odersky's talk a lot and you can assume this post is a bit transcript of his talk;

import odersky.scaladays-cph-2017.talk._
import dotty-docs._
import dotty-example-project._

What is Dotty?

Dotty is a new programming language (yes, it has been written with Scala and will be the Scala 3.0) with a new compiler dotc and all other tools. Dotty aims to simplify and strengthen the core of Scala in the light of Dot Calculus which provides a strong and consistent foundation and specification.

What's new?

After a short summary and references to dotty which means you can go deep inside if you want and curious, we can start explain new features and changes with code examples.

Types

As Odersky explains in his talk, type system of current Scala is very complicated, unsatisfactory and has serious problems such as complex type checking logic and type inference rules. Dotty aims to provide a more strong type system which is more simple, soundness, and consistent than the type system of Scala.

Here is the new features and changes about types in dotty;

  • "Existentional types (T forSome { type A }) and type projections (T#A) are removed from the language because both are unsound and cause problems because of their inconsistent interaction within the type system.", says Odersky.

  • Compound types (T with U) are replaced with Union Types (T | U) and Intersection Types (T & U) which are based on a strong subtyping lattice, where T & U is the lower bound and T | U is the upper bound if T is subtype of U (T <: U).

    T | U 
  /       \
T     <:    U
  \       /
    T & U 
  • Union types represents sum types. Here is a simple example which we define an algebra for division operation:
sealed trait Division
final case class DivisionByZero(msg: String) extends Division
final case class Success(double: Double) extends Division

// Create a type alias for your union types (sum types).
type DivisionResult = DivisionByZero | Success

sealed trait Either[+A, +B]
final case class Left[+A, +B](a: A) extends Either[A, B]
final case class Right[+A, +B](b: B) extends Either[A, B]

def safeDivide(a: Double, b: Double): DivisionResult = {
  if (b == 0) DivisionByZero("DivisionByZeroException") else Success(a / b)
}

def either[A, B](division: Division) = division match {
  case DivisionByZero(m: String) => Left(m)
  case Success(d: Double) => Right(d)
}

val divisionResultSuccess: DivisionResult = safeDivide(4, 2)

// types are commutative: A | B == B | A
val divisionResultFailure: Success | DivisionByZero = safeDivide(4, 0)

either(divisionResultSuccess)
// val res0: Either[String, Double] & Product = Right(2.0)

either(divisionResultFailure)
// val res1: Either[String, Double] & Product = Left(DivisionByZeroException)
  • Intersection Types represents product types. A simple example which shows how you can define a Point with the intersection of two types (X and Y) and calculate the euclidean distance between them by using intersection types:
sealed trait X {
  def x: Double
  def tpe: X
}

sealed trait Y {
  def y: Double
  def tpe: Y
}

// to show intersection types are also commutative
type P = Y & X
type PP = X & Y

final case class Point(x: Double, y: Double) extends X with Y {
  override def tpe: X & Y = ???
}

def euclideanDistance(p1: X & Y, p2: X & Y) = {
  Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2))
}

val p1: P = Point(3, 4)
val p2: PP = Point(6, 8)

euclideanDistance(p1, p2)
// val res2: Double = 5.0
  • Type Lambda is a new feature. In current version of Scala, there is no intentional type lambda support provided by compiler and the community has found that it is possible by using structural types and type projection. However, creating a type lambda in Scala is painful in terms of syntax, but kind-projector compiler plugin helps to make a bit cleaner. You can check this blog post if you want to read more about type lambdas. Instead of type projections which are removed from the language in dotty as said above, built-in type lambda support is here. There are two different way for defining your type lambdas as you can see example below;
type T[+X, Y] = Map[Y, X]

type Tuple = [X] => (X, X)

val m: T[String, Int] = Map(1 -> "1")

val tuple: Tuple[String] = ("a", "b")
  • Named Type Arguments is a new feature of dotty which provides type arguments of methods can be named as well as by position like named value arguments (def three(a: Int = 3): String = "three"). An example of it can be given by using Functors which needs multi type parameters:
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

implicit object listFunctor extends Functor[List] {
  override def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}

def fmap[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Functor[F]): F[B] = F.map(fa)(f)

// here you can set type parameters when you are calling your parameterized functions.
val result: List[Int] = fmap[F = List, A = Int, B = Int](List(1,2,3))(i => i + 1)
// val result: List[Int] = List(2, 3, 4)

// val notCompileExample = fmap[F = List, B = String](List(1,2,3))(i => i + 1)

val compileExample: List[String] = fmap[F = List, B = String](List(1,2,3))(i => (i + 1).toString)
// val compile: List[String] = List(2, 3, 4)

Traits

  • Trait Parameters: In dotty, traits can take parameters. In contrast to abstract vals in the current Scala traits where parameters are initialized after or during the constructor of the concrete type, trait parameters in dotty are evaluated immediately so you can even use them in the constructor of the concrete type if you need.

With this new feature there is no need anymore for early definitions like;

trait Base
class A extends { val msg = "Hello" } with Base {
  def sayHi = msg
}
val a = new A
a.sayHi
// res3: String = Hello

Instead, you can give pass you parameters to the trait directly as in this example;

trait Base(val msg: String)
class A extends Base("Hello")
class B extends Base("Dotty!")

def printMessages(msgs: (A | B)*) = println(msgs.map(_.msg).mkString(" "))

printMessages(new A, new B) 
// Hello Dotty!

One important problem related to trait parameters would be the Inheritance Diamonds again from Odersky's talk;

                        trait T(x: Int)
                      /                 \   
class C extends T(22)          |          trait U extends T
                     \                  /
                        class D extends 
                           C with U

Two rules provide the solution to that problem;

  • traits can never pass parameters to other traits, only classes can.
  • the first class that is implementing trait T (in the inheritance hierarchy) must parameterized it. Not second or third ones.

Enums

Finally, enum support with new shiny enum keyword is arrived in dotty and constructing enums in Scala will not be a pain anymore (in the future). Classical enum java example can be shown as follows;

// taken from: https://github.com/lampepfl/dotty/issues/1970
enum class Planet(mass: Double, radius: Double) {
  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) =  otherMass * surfaceGravity
}

object Planet {
  case MERCURY extends Planet(3.303e+23, 2.4397e6)
  case VENUS   extends Planet(4.869e+24, 6.0518e6)
  case EARTH   extends Planet(5.976e+24, 6.37814e6)
  case MARS    extends Planet(6.421e+23, 3.3972e6)
  case JUPITER extends Planet(1.9e+27,   7.1492e7)
  case SATURN  extends Planet(5.688e+26, 6.0268e7)
  case URANUS  extends Planet(8.686e+25, 2.5559e7)
  case NEPTUNE extends Planet(1.024e+26, 2.4746e7)
}

def calculateEarthWeightOnPlanets(earthWeight: Double) = {
  val mass = earthWeight/Planet.EARTH.surfaceGravity
  for (p <- Planet.enumValues)
    println(s"Your weight on $p is ${p.surfaceWeight(mass)}")
}

calculateEarthWeightOnPlanets(80)
/*
Your weight on MERCURY is 30.22060921607482
Your weight on SATURN is 85.28124310492532
Your weight on VENUS is 72.39992798728365
Your weight on URANUS is 72.41017595115402
Your weight on EARTH is 80.0
Your weight on NEPTUNE is 91.06624579757263
Your weight on MARS is 30.29897472297031
Your weight on JUPITER is 202.44460203965926
*/

Also, with enum feature, ADTs (Algebraic Data Types) and GADTs (Generalized ADT) are supported without any extra cost. You can check the docs to learn more about. A short example to see how you can construct your own ADT with enums is as follows;

enum ListEnum[+A] {
  case Cons[+A](h: A, t: ListEnum[A]) extends ListEnum[A]
  case Empty extends ListEnum[Nothing]
}

val emptyList = ListEnum.Empty
// val res4: ListEnum[Nothing] = Empty

val list = ListEnum.Cons(1, ListEnum.Cons(2, ListEnum.Cons(3, ListEnum.Empty)))
// val res5: ListEnum[Int] = Cons(1,Cons(2,Cons(3,Empty)))

def sum(list: ListEnum[Int]): Int = list match {
  case ListEnum.Empty => 0
  case ListEnum.Cons(h, t) => h + sum(t)
}

println(sum(list))
// 6

Implicits

Implicits are very powerful and useful in terms of program syntesis, but they are also complex and puzzler. Also, they may cause too much repetition in the code by passing implicit parameters during the flow of the code.

  • Implicit conversion One of the puzzler is wrong implicits in the scope which may cause wrong results. To be able to prevent such errors, in dotty, implicit conversion rules are tightened. Any value of type Function1 or its subtypes can be used in implicit conversion at the current Scala. You can check this scala puzzler. In dotty, you need to implement the new abstract class ImplicitConverter[-T, +U] extends Function1[T, U] class for your intented conversion explicitly. And, dotty only considers methods as implicit conversions, implicit vals should not be allowed for conversion.
def convert[T, U](x: T)(implicit converter: ImplicitConverter[T, U]): U = converter(x)

case class IntWrapper(a: Int) extends AnyVal
case class DoubleWrapper(b: Double) extends AnyVal

implicit val IntWrapperToDoubleWrapper = new ImplicitConverter[IntWrapper, DoubleWrapper] {
  override def apply(i: IntWrapper): DoubleWrapper = new DoubleWrapper(i.a.toDouble)
}

// not working in dotty
//implicit val IntToDouble: IntWrapper => DoubleWrapper = int: IntWrapper => DoubleWrapper(int.a.toDouble)

convert(new IntWrapper(42))
// val res6: DoubleWrapper = DoubleWrapper(42.0)
  • Implicit Functions: Since, implicit parameters represent context in general, they cause many repetition. Dotty solves that problem by introducing implicit function types.
trait ImplicitFuntion1[-T, +R] extends Function1[T, R] {
  def apply(implicit t: T): R
}

Now you can pass implicit parameters as implicit functions. For example, you need ExecutionContext when working with scala.concurrent.Future and they are passed as implicit parameters in general. You can now define as implicit functions as this example for your ExecutionContext;

// type alias Contextual
type Contextual[T] = implicit ExecutionContext => T

// sum is expanded to sum(x, y)(ctx)
def asyncSum(x: Int, y: Int): Contextual[Future[Int]] = Future(x + y)
// def asyncSum(x: Int, y: Int): Contextual[concurrent.Future[Int]]

def asyncMult(x: Int, y: Int) = { implicit ctx: ExecutionContext =>
  Future(x * y)
}
// def asyncMult(x: Int, y: Int): implicit concurrent.ExecutionContext => scala.concurrent.Future[Int]

// then if you have implicit in scope, you can call your method as usual.
// compiler automatically convert them to curried implicit parameters as usual. 
// def asyncMult(x: Int, y: Int)(implicit ec: ExecutionContext) = ???

import ExecutionContext.Implicits.global

asyncSum(3, 4).foreach(println)
// 7

asyncMult(3, 4).foreach(println)
// 12

As a bonus, in dotty, implicit parameters can be passed as by-name. By this way, implicit parameters will be evaluated if only the code flow will reach to the point where implicit parameters are needed. In the following example, implicit parser is not evaluated if the string is empty.

sealed trait StringParser[A] {
  def parse(s: String): Try[A]
}

object StringParser {

  def apply[A](implicit parser: StringParser[A]): StringParser[A] = parser

  private def baseParser[A](f: String  Try[A]): StringParser[A] = new StringParser[A] {
    override def parse(s: String): Try[A] = f(s)
  }

  implicit val stringParser: StringParser[String] = baseParser(Success(_))
  implicit val intParser: StringParser[Int] = baseParser(s  Try(s.toInt))

  implicit def optionParser[A](implicit parser: => StringParser[A]): StringParser[Option[A]] = new StringParser[Option[A]] {
    override def parse(s: String): Try[Option[A]] = s match {
      case ""  Success(None) // implicit parser not used.
      case str  parser.parse(str).map(x  Some(x)) // implicit parser is evaluated at here
    }
  }
}

import StringParser._

println(implicitly[StringParser[Option[Int]]].parse("21"))
// Success(Some(21))

println(implicitly[StringParser[Option[Int]]].parse(""))
// Success(None) => implicit parser parameter is not evaluated at this call.

println(implicitly[StringParser[Option[Int]]].parse("21a"))
// Failure(java.lang.NumberFormatException: For input string: "21a")

"Dotty implementation of pattern matching was greatly simplified compared to scalac. From a user perspective, this means that Dotty generated patterns are a lot easier to debug, as variables all show up in debug modes and positions are correctly preserved" from the docs.

Moreover, you do not need to define your extractos only in Option based way, dotty supports 4 different extractor patterns. You can check this example for a complete use cases of these patterns in detail.

Multiversal Equality

Current scala == and != operators are based on Java equals method. It allows you to compare different types without any compiler error or warning. In dotty, universal equality is getting more typesafe with multiversal equality. You need to define an implicit Eq for your intented types to be compared. It only provides compiler level safety, runtime is again based on Java's equals.

package scala
import annotation.implicitNotFound

@implicitNotFound("Values of types ${L} and ${R} cannot be compared with == or !=")
sealed trait Eq[-L, -R]

object Eq extends Eq[Any, Any]

You may want to check Dotty Predef, implicit Eq instances are already on there for primitive types. EqAny is on the predef for just backward compatibility and if you exclude it, you will need explicit Eq instances for your types to compare as shown;

import dotty.DottyPredef.{eqAny => _, _}

// if you do ned define eqIntString, this line does not compile: 
// Values of types Int and String cannot be compared with == or !=
implicit def eqIntString: Eq[Int, String] = Eq
println(3 == "3")

// As default all Sequences are comparable, because of;
// implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[Seq[T], Seq[U]] = Eq
println(List(1, 2) == Vector(1, 2))

class A(a: Int)
class B(b: Int)

val a = new A(4)
val b = new B(4)

implicit def eqAB: Eq[A, B] = Eq
println(a != b)

implicit def eqBA: Eq[B, A] = Eq
println(b == a)

Lazy Vals

Lazy vals are not thread safe in dotty. If you need thread safety, you need to mark your lazy vals with @volatile annotation.

@volatile lazy val xs: List[String] = List("d", "o", "t", "t", "y")

Auto Parameter Tupling of Function Parameters

At last, you do not need pattern-matching decomposition for your function combinators.

val xs: List[String] = List("d", "o", "t", "t", "y")

// instead of
// xs.zipWithIndex.foreach { case (s, i) => println(s"$i: $s") }
xs.zipWithIndex.foreach((s, i) => println(s"$i: $s"))
// 0: d
// 1: o
// 2: t
// 3: t
// 4: y
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment