Dotty’s new REPL

Real World Dotty

Felix Mulder

In This Talk

  • Building a REPL 101
  • The New REPL
  • New Language Features

After this take you’ll know

  • How to build a REPL
  • About features in Scala 3
Slides: felixmulder.com

Questions up front

Repo Activity

88 Contributors

So, how do you build a REPL?

Evaluate expressions & Commands

Expression => Result
IO[String] => Result[String]
type Result[T] = ???

Compiler Crash-Course

  1. Tokenize
  2. Parse AST
  3. Typecheck
  4. Runnable
type Result[T] = ValidatedNel[ErrorMessage, T]

*that aren’t Java

Dotty has union types! 💡

type Result[T] = List[ErrorMessage] | T 

A Compromise

type Result[T] = scala.util.Either[List[ErrorMessage], T]
val readLine: IO[String] = IO { "val x = 5" }

def interpret(input: String): Result[String] = ...
    

val pipeline: IO[String] = for {
  input  <- readLine
  result =  interpret(input).fold(reportErrors, reportResult)
} yield result

pipeline.unsafeRunSync() // "val x: String = 5"
def interpret(input: String): Result[String] =
  compile(input).flatMap(evaluate)

def compile(input: String): Result[tpd.Tree] =
  for {
    exprs     <- parse(input)
    contained <- wrap(exprs)
    typed     <- compile(contained)
  } yield typed

def evaluate(tree: tpd.Tree): Result[String] = ...

Why wrap things?

Need to run the full compiler pipeline

What should we be compiling?

scala> val x = 5

// =>

object rs$l1 {
  val x = 5
}

What should we be compiling?

scala> 5

// =>

object rs$l2 {
  val res0 = 5
}

What should we be compiling?

scala> class Foo

// =>

object rs$l3 {
  class Foo
}

Is that it?

scala> val y = x

// =>

object rs$l4 {
  import rs$l1._
  val y = x // error: not found: value x
}

Is that it?

val y = x

// =>

object rs$l4 {
  import rs$l1._, rs$l2._, rs$l3._
  val y = x                             
}

What about implicits?

scala> implicit val x: String = "wrong"
scala> implicit val y: String = "right"
scala> implicitly[String]

// =>

object rs$l1 { implicit val x: String = "wrong" }

object rs$l2 {
  import rs$l1._
  implicit val y: String = "right"
}

object rs$l3 {
  import rs$l1._, rs$l2._
  val res0 = implicitly[String] // error: ambiguous implicit values
}
🙈

Dotty’s Concept of Context

  • Context is analogous to scalac’s Global
  • Local
  • Scope
  • Owner
  • Settings
  • Immutable
package example

class A

class B {
                                                 
 def f = ???
 def g = ???
}
package example {
  // Context(owner = example, scope = Scope(A, B))
  class A

  class B {
   // Context(owner = B, scope = Scope(A, B, f, g))
   def f = ???
   def g = ???
  }
}
object rs$l1 { implicit val x: String = "wrong" }

object rs$l2 {
  // Context(owner = rs$l2, scope = ShadowScope(rs$l1._))
  import rs$l1._
  implicit val y: String = "right"
}

object rs$l3 {
  // Context(owner = rs$l2, scope = ShadowScope(rs$l2._, rs$l1._))
  import rs$l1._, rs$l2._
  val res0 = implicitly[String] // val res0: String = "right" 😁👍
}
def compile(input: String): Result[tpd.Tree] =
  for {
    exprs     <- parse(input)
    contained <- wrap(exprs)
    typed     <- compile(contained)
  } yield typed
def compile(tree: untpd.Tree)(implicit ctx: Context): tpd.Tree = {

  def addMagicImports(initCtx: Context): Context =
    (initCtx /: lineNumbers) { (ctx, line) =>
      ctx.setNewScope.setImportInfo(importRef(line))
    }

  // Use dotty internals to compile the `tree` => then
  // return the typed version of it
}
def interpret(input: String): Result[String] =
  compile(input).flatMap(evaluate)

def evaluate(tree: tpd.Tree): Result[String] = {
  // 1. Render definitions: class, trait, object, type, def
  // 2. Render values: val, var
}
def evaluate(tree: tpd.Tree): Result[String] = {
  val defs = ctx.atPhase(ctx.typerPhase.next) {
    tree.symbol
      .find(isWrapper).toList
      .flatMap(collectDefs)
      .map(renderDefs)
  }

  val values = renderValues(tree)

  // Return everything separated by newlines:
  (defs ++ values).mkString("\n")
}

What does Haskell do?

Interpreter

“Let’s just use Reflection” - Java devs
def renderValues(tree: tpd.Tree): List[String] = {
  def valueOf(sym: Symbol): Option[String] = { ...  }

  collectValues(tree).map { symbol =>
    val dcl = symbol.showUser
    val res = if (symbol.is(Lazy)) Some("<lazy>") else valueOf(symbol)

    res.map(value => show"$dcl = $value")
  }
}
def valueOf(sym: Symbol): Option[String] = {
  val wrapperName = sym.owner.name
  val wrapper = Class.forName(wrapperName, true, classLoader)

  val res =
    wrapper
      .getDeclaredMethods.find(_.getName == sym.name + "Show")
      .map(_.invoke(null).toString)

  if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
    None
  else res
}
trait Show[-T] {
  def show(t: T): String
}

What should we be compiling?

scala> val x = 5

// =>

object rs$l1 {
  val x = 5
  def xShow = x.show
}

Trivia - why a def?

def valueOf(sym: Symbol): Option[String] = {
  val wrapperName = sym.owner.name
  val wrapper = Class.forName(wrapperName, true, classLoader)

  val res =
    wrapper
      .getDeclaredMethods.find(_.getName == sym.name + "Show")
      .map(_.invoke(null).toString) // Initializes the object 🙊

  if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
    None
  else res
}
Expression => Result

🎩 + 🐿 == `Ship it!`

New Language Features

And how they were (mis-)used in this project

Dotty has union types! 💡

type Result[T] = List[ErrorMessage] | T 
type Result[T] = List[ErrorMessage] | T

implicit class ResultOps[A](res: Result[A]) extends AnyVal {
  def flatMap[B](f: A => Result[B]): Result[B] = res match {
    case err: List[ErrorMessage] => err // warning: type erasure
    case a: A => f(a)                   // warning: match on generic type
  }
}
type Result[T] = Errors | T
private case class Errors(values: List[ErrorMessage])

implicit class ResultOps[A](res: Result[A]) extends AnyVal {
  def flatMap[B](f: A => Result[B]): Result[B] = res match {
    case err: Errors => err
    case a: A @unchecked => f(a)
  }

  ...
}
for { x <- 1 } yield 1

🙈

1 <:< Result[T]

1 <:< (Errors | T)

(1 <:< Errors) || (1 <:< T)

false || (1 <:< T)

true

When not to use a union type

  • Instead of Either
  • Instead of Coproduct
  • When part of the union is generic

When to use union types

  • Anonymous ADTs
  • Unordered disjunctions
  • Value Enumerations
  • Dynamic Language Interop
enum Message {
  case PlainMessage[A](value: A)
  case ComputableMessage[A](value: () => A)
  case NoMessage
}

type SomeMessage[A] = PlainMessage[A] | ComputableMessage[A]

def log[A : Broker](msg: SomeMessage): IO[Unit] =
  msg match {
    case msg: PlainMessage      => IO { println(msg) }
    case msg: ComputableMessage => IO { println(msg.compute) }
  }

When to use union types

  • Anonymous ADTs
  • Unordered disjunctions
  • Value Enumerations
  • Dynamic Language Interop
(String | Int) =:= (Int | String)

When to use union types

  • Anonymous ADTs
  • Unordered disjunctions
  • Value Enumerations
  • Dynamic Language Interop
enum Days {
  case Monday
  case Tuesday
  case Wednesday
  case Thursday
  case Friday
  case Saturday
  case Sunday
}

type Weekday = Monday.type   | Tuesday.type | ... | Friday.type
type Weekend = Saturday.type | Sunday.type
    

Sadly

Allow singleton types in union types

#1551

When to use union types

  • Anonymous ADTs
  • Unordered disjunctions
  • Value Enumerations
  • Dynamic Language Interop
type UndefOr[A] = A | Unit

Improved Type Inference

val res: Either[Exception, Int] = Right(1)

res.map(_ + 1)
    

Improved Type Inference

val res: Either[Exception, (Int, String)] = Right((1, "foo"))

res.map((i, str) => (i + 1, str + "bar"))
// error: found (Int, String) => (Int, String),
//        required ((Int, String)) => ?

res.map { case (i, str) => (i + 1, str + "bar") }
    
implicit class ListOps[A](val xs: List[A]) extends AnyVal {
  def myFoldLeft1[B](init: B, f: (B, A) => B): B = ...

  def myFoldLeft2[B](init: B)(f: (B, A) => B): B = ...
}

List(1, 2, 3).myFoldLeft2(0)(_ + _)
List(1, 2, 3).myFoldLeft1(0, _ + _)
// error: missing parameter type for expanded function
//        ((x$1: , x$2: Int) => x$1.$plus(x$2))
List(1, 2, 3).myFoldLeft1(0, (a: Int, x: Int) => a + x)
List(1, 2, 3).myFoldLeft1[Int](0, _ + _)

xs.foldRight(List.empty)(_ :: _)
// res0: List[Any](1, 2, 3)

def foo: List[Int] = xs.foldRight(List.empty)(_ :: _)
    
🤗
trait Foo[A] { type B }

def foo[A](t: A)
          (implicit f: Foo[A], m: Monoid[f.B]): f.B = m.zero

A Note on Contraviariance

trait Show[-T] {
  def apply(t: T): String
}
trait Show[-T] {
  def apply(t: T): String
}

class A
class B extends A
class C extends B

implicit val showAny = new Show[Any] { def apply(any: Any) = "showing Any" }
implicit val showA   = new Show[A]   { def apply(a: A)     = "showing A" }
implicit val showB   = new Show[B]   { def apply(b: B)     = "showing B" }
implicit val showC   = new Show[C]   { def apply(c: C)     = "showing C" }

implicitly[Show[C]].apply(new C) // res: "showing Any"
trait Show[-T] {
  def apply(t: T): String
}

class A
class B extends A
class C extends B

implicit val showAny: Show[Any] = new { def apply(any: Any) = "showing Any" }
implicit val showA:   Show[A]   = new { def apply(a: A)     = "showing A" }
implicit val showB:   Show[B]   = new { def apply(b: B)     = "showing B" }
implicit val showC:   Show[C]   = new { def apply(c: C)     = "showing C" }

implicitly[Show[C]].apply(new C) // res: "showing C"

State of Dotty

  • Already feels very stable

  • 0.X-releases

  • Macro system coming soon™

  • Try it out!

    $ sbt new lampepfl/dotty.g8
    
    $ brew install lampepfl/brew/dotty

Thank you!