Outline

• Existential types Abstract type members in a nutshell
• Use cases
• Encapsulation
• Modularization
• Type families
• Summary

Expression

A problem well put is half-solved

John Dewey.

Modularization and generalization

• in practice, sub-problems are:
• rarely independent of each other
• often similar

Means of abstraction

• Define something in terms of some unknown other thing
def identity[A](a: A): A

value type
parameter
abstract member

Value parameters

}
def printArea(disc: Disc): Unit = println(disc.area)
val disc = new Disc(123)
printArea(disc)

Scala square of abstraction (2)

value type
parameter (x: Int) (f: Int => Int)
abstract member

Abstract value members

abstract class Disc {

}
def printArea(disc: Disc): Unit = println(disc.area)
val disc = new Disc { val radius = 123 }
printArea(disc)

Scala square of abstraction (3)

value type
parameter (x: Int) (f: Int => Int)
abstract member def x: Int def f(y: Int): Int

Parameters vs abstract members

• Parameters and abstract members are equivalent means of abstraction
• Parameters come more from the functional programming paradigm
• Abstract members come more from the object oriented programming paradigm

Type parameters

trait Invertible[A] {
def invert(a: A): A
}
def invertLaw[T](t: T)(implicit invertible: Invertible[T]): Unit =
assert(invertible.invert(invertible.invert(t)) == t)
implicit val invertibleInt: Invertible[Int] =
new Invertible[Int] { def invert(n: Int) = -n }

invertLaw(42)

Scala square of abstraction (4)

value type
parameter (x: Int) (f: Int => Int) [A] [F[_]]
abstract member def x: Int def f(y: Int): Int

Abstract type members

trait Invertible {
type A
def invert(a: A): A
}
def invertLaw[T](t: T)(implicit invertible: Invertible { type A = T }): Unit =
assert(invertible.invert(invertible.invert(t)) == t)
implicit val invertibleInt: Invertible { type A = Int } =
new Invertible { type A = Int; def invert(n: Int) = -n }

invertLaw(42)

Scala square of abstraction (5)

value type
parameter (x: Int) (f: Int => Int) [A] [F[_]]
abstract member def x: Int def f(y: Int): Int type A type F[_]

Scala interface for *nix files API

object UnixFiles {
def open(path: String): Int = …
def read(file: Int): String = …
def close(file: Int): Int = …
}
UnixFiles.read(42) // I can forge a file descriptor!
}

Hiding the File type

trait UnixFiles {
type File
def open(path: String): File
def close(file: File): Int
}
def readSomething(files: UnixFiles): Unit = {
//         ^^
// Error: Type mismatch;
//  found: Int
//  required: files.File
}

Hiding the File type

def readSomething(files: UnixFiles): Unit = {
val file = files.open("/foo")
files.read(file) // I have no other choice than
// calling the `open` method to
// get a `File` instance
}

Why is it different from using an opaque type?

object UnixFiles {

sealed trait File
private case class FileImpl(value: Int) extends File

def open(path: String): File = {
…
FileImpl(…)
}

def read(file: File): String = file match {
case FileImpl(fileImpl) => …
}

}

Why is it different from using an opaque type?

• Casts everywhere
• Also “hidden” from the point of view of implementers!

Encapsulation – summary

• Abstract type members are an effective way to make a type opaque from the “outside”

When to use this technique?

• When you want to hide the actual implementation type of an abstraction

User interface Component

trait Component[State] {
def view(state: State): Html
}

Counter component

object Counter extends Component[Int] {

def view(state: Int): Html =
<span>value = { state }</span>

// … other methods defining how to update the state

}

Container component

class Container[State](child: Component[State]) extends Component[State] {

def view(state: State): Html =
<div class="container">{ child.view(state) }</div>

}

Tabs component

class Tabs(children: List[(String, Component[_]]))
extends Component[Tabs.State[_]] {

def view(state: Tabs.State[_]): Html = …

}

object Tabs {

case class State[S](child: Component[S], childState: S)

}

Tabs component implementation

class Tabs(children: List[(String, Component[_]]))
extends Component[Tabs.State[_]] {

def view(state: Tabs.State[_]): Html =
<div class="tabs">
<ul>{ for ((label, _) <- children) yield <li>{ label }</li> }</ul>
<div>
{ state.child.view(state.childState) }
</div>
</div>

}

Tabs component implementation

class Tabs(children: List[(String, Component[_]]))
extends Component[Tabs.State[_]] {

def view(state: Tabs.State[_]): Html =
<div class="tabs">
<ul>{ for ((label, _) <- children) yield <li>{ label }</li> }</ul>
<div>
{ state.child.view(state.childState) }
//                         ^^^^^^^^^^^^^^^^
// type mismatch;
//  found   : state.childState.type (with underlying type _\$1)
//  required: _\$1
</div>
</div>

}

Tabs component implementation

class Tabs(children: List[(String, Component[_]]))
extends Component[Tabs.State[_]] {

def view(state: Tabs.State[_]): Html = {

def helper[A](s: Tabs.State[A]): Html =
s.child.view(s.childState)

<div class="tabs">
<ul>{ for ((label, _) <- children) yield <li>{ label }</li> }</ul>
<div>{ helper(state) }</div>
</div>
}

}

Redesign Component with abstract type members

trait Component {

type State

def view(state: State): Html

}

Counter (again)

object Counter extends Component {

type State = Int

def view(state: State): Html = <span>value = { state }</span>

// … other methods defining how to update the state

}

Container (again)

trait Container extends Component {

val child: Component

type State = child.State

def view(state: State): Html =
<div class="container">{ child.view(state) }</div>

}

Tabs – state (1)

class Tabs(children: List[(String, Component])) extends Component {

case class State(child: Component)(childState: child.State)

}
• We can not use a case class to model the State because of SI-5712

Tabs – state (2)

class Tabs(children: List[(String, Component])) extends Component {

trait State {
val child: Component
def childState: child.State
}

}

Tabs – view implementation

class Tabs(children: List[(String, Component])) extends Component {

def view(state: State): Html =
<div class="tabs">
<ul>{ for ((label, _) <- children) yield <li>{ label }</li> }</ul>
<div>
{ state.child.view(state.childState) }
</div>
</div>

}

Modularization – summary

• Abstract members (both types and values) are useful to encapsulate module internals
• Path-dependent types are used to nest modules
• Don’t work with class constructors (see SI-5712)

When to use this technique?

• When you see lots of wildcard type parameters

Naive solution

trait Plot {
def setDataset(dataset: Dataset): Unit
def getDataset: Dataset
}

trait Dataset

Naive solution – XYPlot

trait XYPlot extends Plot {
private var _dataset: XYDataset = _
def setDataset(dataset: Dataset) = _dataset = dataset.asInstanceOf[XYDataset]
def getDataset = _dataset
}

case class XYDataset(series: List[Series]) extends Dataset
case class Series(values: List[(Double, Double)])

Solution with abstract type members

trait Plot {
type Dataset
def setDataset(dataset: Dataset): Unit
def getDataset: Dataset
}

XYPlot (again)

trait XYPlot extends Plot {

case class Dataset(series: List[Series])
case class Series(values: List[(Double, Double)])

private var _dataset: Dataset = _

def setDataset(dataset: Dataset) = _dataset = dataset
def getDataset = _dataset

}

Type families – summary

• Abstract type members can be refined along with the specialization of a class

When to use this technique?

• When you want to consistently specialize several types

Summary

• Existential types are hidden polymorphic types
• We can encode them in Scala using abstract type members
• They can be useful in several situations
• encapsulation
• modularization
• family polymorphism

Defining a language as a data type

sealed trait Expr
case class Lit(x: Double) extends Expr
case class Add(lhs: Expr, rhs: Expr) extends Expr
• Values of type Expr are descriptions of additions

Evaluating programs

def eval(expr: Expr): Double =
expr match {
case Lit(x)        => x
case Add(lhs, rhs) => eval(lhs) + eval(rhs)
}

Pretty-printing programs

def show(expr: Expr): String =
expr match {
case Lit(x)        => x.toString
case Add(lhs, rhs) => print(lhs) ++ " + " ++ print(rhs)
}

Generalizing interpreters

def fold[A](lit: Double => A, add: (A, A) => A): Expr => A = {
case Lit(x)        => lit(x)
}
val eval = fold[Double](identity, _ + _)

val show = fold[String](_.toString, _ ++ " + " ++ _)
• Functions processing Expr values (“folds”) are interpretations of the language

Alternative encoding: language definition

trait ExprDsl {
type Expr
def lit(x: Double): Expr
def add(lhs: Expr, rhs: Expr): Expr
}
trait Program extends ExprDsl {
}

Alternative encoding: evaluation interpreter

trait ExprDsl {
type Expr
def lit(x: Double): Expr
def add(lhs: Expr, rhs: Expr): Expr
}
trait Eval extends ExprDsl {
type Expr = Double
def lit(x: Double) = x
def add(lhs: Double, rhs: Double) = lhs + rhs
}

Alternative encoding: interpreting programs

new Program with Eval {
println(twoPlusThree) // 5
}
new Program with Show {
println(twoPlusThree) // "2 + 3"
}

AlternativeFinally tagless encoding

See also [C. Hofer et al. 2008], [J. Carette et al. 2009] and [B. Oliveira 2012]

Finally tagless encoding – extensibility

trait MulDsl extends ExprDsl {
def mul(lhs: Expr, rhs: Expr)
}
trait MulEval extends Mul with Eval {
def mul(lhs: Double, rhs: Double) = lhs * rhs
}

Type parameters vs type members (1)

trait HttpEndpoints[Req, Resp, Headers, Url] {
…
}
extends HttpEndpoints[Req, Resp, Headers, Url] {
…
}

Type parameters vs type members (2)

trait HttpEndpoints {
type Req
type Resp
type Url
…
}
trait Program extends HttpEndpoints

Type parameters vs type members (3)

trait HttpEndpoints {
type Req <: ReqLike
type Resp
type Url
…
}
trait Program extends HttpEndpoints

Finally tagless encoding – summary

• Extensible way to define embedded languages
• Heavily relies on object-oriented features
• abstract methods and abstract type members

When to use this technique?

• When you want to separate description from (possibly multiple) interpretation(s)