Why Scala?

Marc Saegesser (@marcsaegesser)

June 3, 2018

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
  • Open Source
    • Standard Libraries
    • Lightbend Libraries
    • Compiler

Functional Programming?

  • Functions are 'first class' entities
    • Functions can be assigned to variables
    • Passed as arguments to functions
    • Returned from functions
    • Defined within other functions
  • Pure functions
    • Like mathetmatical functions
    • Given the same arguments always return the same result
    • No side effects
    • Referential transparency
  • Functions provide things that are built in to other languages
    • Control flow
    • Error handling
def factorial(x: BigInt): BigInt =
  if(x == 0)
    1
  else
    x * factorial(x-1)

Functions

// 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

Traits, Classes and Objects

Traits, Classes and Objects

package org.saegesser.puzzle

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

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

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

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

Traits, Classes and Objects

package org.saegesser.puzzle

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

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

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

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

case class FreeTile(label: String, edges: Edges) extends Tile {
  val rotations = edges.rotations

  def withConstraint(c: Constraint): Vector[FixedTile] = {
    rotations
      .filter { r => matchesConstraint(r, c) }
      .map { es => FixedTile(label, es) }
  }
}

Traits, Classes and Objects

package org.saegesser.puzzle

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

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

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

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

case class FreeTile(label: String, edges: Edges) extends Tile {
  val rotations = edges.rotations

  def withConstraint(c: Constraint): Vector[FixedTile] = {
    rotations
      .filter { r => matchesConstraint(r, c) }
      .map { es => FixedTile(label, es) }
  }
}

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

Traits, Classes and Objects

package org.saegesser.puzzle

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

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

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

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

case class FreeTile(label: String, edges: Edges) extends Tile {
  val rotations = edges.rotations

  def withConstraint(c: Constraint): Vector[FixedTile] = {
    rotations
      .filter { r => matchesConstraint(r, c) }
      .map { es => FixedTile(label, es) }
  }
}

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

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

Traits, Classes and Objects

class Board(val tiles: Vector[Tile]) {
  // ...
  def requiredEdgeAt(idx: Int, side: EdgeSide): Option[EdgeValue] =
    tiles(idx) match {
      case FixedTile(_, edges) => Some(matchingEdgeValue(edges.side(side)))
      case FreeTile(_, _)      => None
    }

  def constraintsFor(idx: Int): Constraint = {
    adjacencies(idx) match { case (t, r, b, l) =>
      Constraint(
        t.flatMap(e => requiredEdgeAt(e.idx, e.side)),
        r.flatMap(e => requiredEdgeAt(e.idx, e.side)),
        b.flatMap(e => requiredEdgeAt(e.idx, e.side)),
        l.flatMap(e => requiredEdgeAt(e.idx, e.side))
      )
    }
  }
}

Pattern Matching and Destructuring

class Board(val tiles: Vector[Tile]) {
  // collect all the Free tiles witih their board positions.
  val freeTiles = tiles.zipWithIndex.collect { case (t: FreeTile, i: Int) => (t, i) }

  // ...
}

def boardsFrom(board: Board): Vector[Board] = {
  @inline
  def updateTiles(ts: Vector[Tile], free: Tile, fixed: Tile, 
                  currIdx: Int, destIdx: Int): Vector[Tile] = {
    if(currIdx != destIdx) ts.updated(destIdx, free).updated(currIdx, fixed)
    else                   ts.updated(currIdx, fixed)
  }

  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())
}

An Example

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

Option and Either and Try, oh my!

They're way less scary than lions and tigers and bears

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

val aValue = Some(42)
val negValue = Some(-42)
val noValue = None

Option and Either and Try, oh my!

They're way less scary than lions and tigers and bears

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

val aValue = Some(42)
val negValue = Some(-42)
val noValue = None

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

Option and Either and Try, oh my!

They're way less scary than lions and tigers and bears

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

val aValue = Some(42)
val negValue = Some(-42)
val noValue = None

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 and Either and Try, oh my!

They're way less scary than lions and tigers and bears

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 and Either and Try, oh my!

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 and Either and Try, oh my!

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!")

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 and Either and Try, oh my!

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)

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

Option and Either and Try, oh my!

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 and Either and Try, oh why!

An ugly example

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

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

Option and Either and Try, oh why!

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

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

Option and Either and Try, oh why!

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

object Connection {
  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

object Connection {
  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
}

class Connection {
  import Connection._

  var currentState: ConnectionState = Disconnected

  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

Important Libraries

  • ScalaTest - Unit test framework
  • ScalaCheck - Property based testing
  • ScalaZ, Cats - Advanced FP for Scala
  • STM - Software Transactional Memory
  • Argonaut, Circe - JSON parsing
  • Apache Spark - Big data

Conclusion

  • Reasoning about and understanding code
  • Domain modelling
  • Design and implementation
  • Testing and validation
  • Maintainability
  • Error handling
  • Code re-use

Functions and Types improve ...

Postlude

Our situation is analogous to that of someone who has learned the rules for how the pieces move in chess but knows nothing of typical openings, tactics, or strategy. Like the novice chess player, we don’t yet know the common patterns of usage in the domain. We lack the knowledge of which moves are worth making ... We lack the experience to predict the consequences of making a move ...

Structure and Interpretation of Computer Programs (sec 1.2)

References