Why Scala?

Marc Saegesser (@marcsaegesser)

August 27, 2019

What is Scala?

  • Scala is a JVM language*
    • Compiles to Java byte code
    • Runs anywhere there's  JVM
    • Interoperability between Java and Scala
  • Multi-paradigm language
    • Object oriented programming
    • Functional programming
def factorial(x: BigInt): BigInt =
  if(x == 0)
    1
  else
    x * factorial(x-1)

Some Simple Scala

// In Java
import java.util.BigInteger

BigInteger factorial(BigIteger x) {
  if(x == BigInteger.ZERO) {
    return BigInteger.ONE;
  } else {
    return x.multiply(factorial(x.subtract(BigInteger.ONE));
  }
}

The equivalent in Java

Programming in Scala (Chapter 2) http://a.co/c1HfcNR

Growing a Language

A 'Real-World' Example

Sort Of

package puzzle

sealed trait Tile {
  def label: String
  def edges: Edges

  def withConstraint(constraint: Constraint): Vector[FixedTile]

  def show: String = s"$label. $edges"

  def matchesConstraint(c: Constraint): Boolean = // ...
}

case class FreeTile(label: String, edges: Edges) extends Tile {
  val rotations: Vector[FixedTile] = edges.rotations.map(r => FixedTile(label, r))

  def withConstraint(c: Constraint): Vector[FixedTile] =
    rotations.filter(_.matchesConstraint(c))
}

case class FixedTile(label: String, edges: Edges) extends Tile {
  def withConstraint(c: Constraint): Vector[FixedTile] =
    if(matchesConstraint(c)) Vector(this)
    else                     Vector()
}

object Tile {
  def apply(label: String, edges: Array[String]): Tile = FreeTile(label, Edges(edges))
}

Traits, Classes and Objects














case class FreeTile(label: String, edges: Edges) extends Tile {
  val rotations: Vector[FixedTile] = edges.rotations.map(r => FixedTile(label, r))

  def withConstraint(c: Constraint): Vector[FixedTile] =
    rotations.filter(_.matchesConstraint(c))
}

Traits, Classes and Objects

trait BoardBuilder {
  def boardsFrom(board: Board): Vector[Board]
}

object SymmetryBuilder extends BoardBuilder {
  def updateTiles(ts: Vector[Tile], free: Tile, fixed: Tile, 
                  currIdx: Int, destIdx: Int): Vector[Tile] = ???

  def boardsFrom(board: Board): Vector[Board] = {
    board.freeTiles.headOption.map { case (free, point) => // The first free tile and its index
      val c = board.constraintsFor(point)                  // Constraints for point
      board.freeTiles
        .map { case (t, i) => (t.withConstraint(c), i) }   // Constraint search for point
        .filterNot { case (ts, _) =>
          ts.isEmpty ||                                    // Ignore tiles with no matches
          (point == 0 && ts.head.label >= "7") ||          // Symmetry constraints
          ((point == 2 || point == 6 || point == 8) && (ts.head.label < board.tiles(0).label))
        }.flatMap { case (ts, i) =>
            ts.map(t => new Board(updateTiles(board.tiles, free, t, point, i)))
        }
    }.getOrElse(Vector())
  }
}

The Core Algorithm

sealed trait Option[A]  { // ... }
case class   Some(a: A) extends Option[A] { // ... }
case object  None       extends Option[A] { // ... }

Option, Either and Try

sealed trait Option[A]  { // ... }
case class   Some(a: A) extends Option[A] { // ... }
case object  None       extends Option[A] { // ... }

val aValue: Option[Int]   = Some(42)
val negValue: Option[Int] = Some(-42)
val noValue: Option[Int]  = None

Option, Either and Try

sealed trait Option[A]  { // ... }
case class   Some(a: A) extends Option[A] { // ... }
case object  None       extends Option[A] { // ... }

val aValue: Option[Int]   = Some(42)
val negValue: Option[Int] = Some(-42)
val noValue: Option[Int]  = None

trait Option[A] {
  def map[B](f: A => B): Option[B]

}

aValue.map(_.toString)  // Some("42")
noValue.map(_.toString) // None

Option, Either and Try

sealed trait Option[A]  { // ... }
case class   Some(a: A) extends Option[A] { // ... }
case object  None       extends Option[A] { // ... }

val aValue: Option[Int]   = Some(42)
val negValue: Option[Int] = Some(-42)
val noValue: Option[Int]  = None

trait Option[A] {
  def map[B]    (f: A => B        ): Option[B]
  def flatMap[B](f: A => Option[B]): Option[B]
}

aValue.map(_.toString)  // Some("42")
noValue.map(_.toString) // None

aValue.flatMap   { x => if(x > 0) Some(x.toString) else None }  // Some("42")
negValue.flatMap { x => if(x > 0) Some(x.toString) else None }  // None
noValue.flatMap  { x => if(x > 0) Some(x.toString) else None }  // None

Option, Either and Try

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

Option, Either and Try

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

val aRight: Either[String, Int] = Right(42)
val aLeft: Either[String, Int]  = Left("Oops!")

Option, Either and Try

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

val aRight: Either[String, Int] = Right(42)
val aLeft: Either[String, Int]  = Left("Oops!")

trait Either[A, B] {
  def map[C]             (f: B => C            ): Either[A, C]
  def flatMap[A1 >: A, C](f: B => Either[A1, C]): Either[A1, C]
}

aRight.map(_*2)  // Right(84)
aLeft.map(_*2)   // Left("Oops")

aRight.flatMap { x => if(x >= 0) Right(x*2) else Left("Negative") }  // Right(84)

Option, Either and Try

sealed trait Try[A]
case class   Success[A](a: A)         extends Try[A]
case class   Failure[A](t: Throwable) extends Try[A]

val success = Try { 2 / 1 } // Success(2)
val fail    = Try { 2 / 0 } // Failure(java.lang.ArithmeticException: / by zero)

trait Try[A] {
  def map[B]    (f: A => B     ): Try[B]
  def flatMap[B](f: A => Try[C]): Try[C]
}

success.map(_*2)  // Success(4)
fail.map(_*2)     // Failure(java.lang.ArithmeticException: / by zero)

Option, Either and Try

def getDBConnection(): Connection
def getUserIdForName(conn: Connection, name: UserName): Int
def updateUserAddress(conn: Connection, user: Int, address: Address): ConfirmationCode

val conn = getDBConnection()
if (conn != null) {
  val user = getUserIdForName(conn, name)
  if (user != -1) {
    val conf = updateUserAddress(conn, user, address)
    if (conf != null) {
      // Report success
    } else {
      // Report address change failure
    }
  } else {
    // Report get user failure
  }
} else {
  // Report get connection failure
}

Option, Either and Try

An ugly example

def getDBConnection(): Either[Throwable, Connection]
def getUserIdForName(conn: Connection, name: UserName): Either[Throwable, UserId]
def updateUserAddress(conn: Connection, user: UserId, 
                      address: Address): Either[Throwable, ConfirmationCode]

def updateAddress(name: UserName, newAddress: Address): Either[Throwable, ConfirmationCode] = ???

Option, Either and Try

def getDBConnection(): Either[Throwable, Connection]
def getUserIdForName(conn: Connection, name: UserName): Either[Throwable, UserId]
def updateUserAddress(conn: Connection, user: UserId, 
                      address: Address): Either[Throwable, ConfirmationCode]

def updateAddress(name: UserName, newAddress: Address): Either[Throwable, ConfirmationCode] =
  for {
    c <- getDBConnection()
    u <- getUserIdForName(c, name)
    r <- updateUserAddress(c, u, address)
  } yield r

Option, Either and Try

def getDBConnection(): Try[Connection]
def getUserIdForName(conn: Connection, name: UserName): Try[UserId]
def updateUserAddress(conn: Connection, user: UserId, address: Address): Try[ConfirmationCode]

def updateAddress(name: UserName, newAddress: Address): Try[ConfirmationCode] =
  for {
    c <- getDBConnection()
    u <- getUserIdForName(c, name)
    r <- updateUserAddress(c, u, address)
  } yield r

A Digression on Types of Types

Option, Either, Try are examples of Sum types

AKA:  Union types or Disjoint Union types or Variants

Tuples, classes, etc. are examples of Product types

Together Product and Sum types are called

Algebraic Data Types (ADTs)

An Example

Implementing a network client

  • A network socket
  • A heartbeat timer
  • A connect timer
  • A reconnect timer
  • A reconnect count

Requirements

An Example

Implementing a network client

case class ConnectionState(
  state: State,
  socket: Option[Socket], 
  hbTimer: Option[Timer], 
  connTimer: Option[Timer], 
  reconnTimer: Option[Timer],
  rconnCount: Option[Int]
)
sealed trait ConnectionState
case object  Disconnected extends ConnectionState
case class   Connecting(s: Socket, connectTimer: TimerTask) extends ConnectionState
case class   Connected(s: Socket, heartBeatTimer: TimerTask) extends ConnectionState
case class   Reconnecting(s: Socket, attempts: Int, connectTimer: TimerTask) extends ConnectionState
case class   ReconnectWait(attempts: Int, reconnectTimer: TimerTask) extends ConnectionState

Make Illegal State Unrepresentable

sealed trait ConnectionState
case object  Disconnected extends ConnectionState
case class   Connecting(s: Socket, connectTimer: TimerTask) extends ConnectionState
case class   Connected(s: Socket, heartBeatTimer: TimerTask) extends ConnectionState
case class   Reconnecting(s: Socket, attempts: Int, connectTimer: TimerTask) extends ConnectionState
case class   ReconnectWait(attempts: Int, reconnectTimer: TimerTask) extends ConnectionState

def connect(host: String): Try[Unit] =
  currentState match {
    case Disconnected           => ???
    case Connecting(s, t)       => ???
    case Connected(s, hbt)      => ???
    case Reconnecting(s, a, ct) => ???
    case ReconnectWait(a, rct)  => ???
  }

Make Illegal State Unrepresentable

Make Illegal State Unrepresentable

Video: Effective ML by Yaron Minsky

https://youtu.be/-J8YyfrSwTk

So, Why Scala?

  • Expressive type system
    • Rich domain models express constraints in Types
    • Type driven development
    • Improve maintainability
    • Easier refactoring
  • Programming with functions
    • Error handling - Errors are values
    • Reason about code behavior
    • Code re-use

The Bigger Picture

References and Further Reading

Why Scala?

By Marc Saegesser

Why Scala?

Chicago Java Users Group March 26, 2019

  • 446