Scala is not Java

Mateusz Kubuszok > Scalac

Agenda

  • Scala's features
  • some FP "design patterns"
  • some OOP design patterns
  • (not) porting Java approach to Scala

Scala's features

var, val and type inference

void publish(
    Message message,
    Publisher publisher) {
  Timestamp timestamp = makeTimestamp();
  publisher.send(message, timestamp);
}
def publish(
    message: Message
    publisher: Publisher): Unit = {
  val timestamp = makeTimestamp()
  publisher.send(message, timestamp)
}

functions

def myFun1(int: Int): String = int.toString
val myFun2: String => String = _.toUpperCase
val myFun3 = myFun1 andThen myFun2
List(1, 2, 3).map(myFun3)

pattern matching

result match {
  case Left(error)  => handleError(error)
  case Right(value) => handleResult(value)
}

traits: sealed, ADT, mixins

sealed trait Stack[+T]
case object Empty extends Stack[Nothing]
case class Top[+T](value: T, next: Stack[T])
class Printer {
  def result: List[Either[String, String]] = ???
  def print: Unit = result.foreach(println)
}
trait ErrFilter { self: Printer => def result = super.result.filter(_.isRight) }
trait OutputLimiter { self: Printer => def result = super.result.take(20) }
new ResultPrinter with ErrFilter with OutputLimiter

immutable data structures

case class Cheese(name: String, weightInG: Double)
val cheese1 = Cheese("Gouda", 12.6)
val cheese2 = cheese1.copy(name = "Cheddar")
val list1 = "a" :: "b" :: "c" :: Nil
val list2 = list1 ++ List("d", "e", "f")

for comprehension

for {
  i <- List(1, 2, 3)
  j <- List(4, 5, 6)
  k <- List(7, 8, 9)
} yield i * j * k
List(1, 2, 3) flatMap { i =>
  List(4, 5, 6) flatMap { j =>
    List(7, 8, 9) map { k =>
      i * j * k
    }
  }
}

Implicits

implicit val executionContext: ExecutionContext = ...
def asyncResult(implicit ec: ExecutionContext): Future[String] = ???
implicit class StringDecorator(string: String) {
  def cleanString = string.toLowerCase.trim
}
"  MY STRING  ".cleanString
sealed trait Nat
case object Zero extends Nat
case class Succ[N <: Nat](value: N) extends Nat

implicit val natZero: Zero = Zero
implicit def natSucc[N <: Nat](implicit nat: N): Succ[N] = Succ(n)

implicitly[Succ[Succ[Succ[Zero]]]] // Succ(Succ(Succ(Zero)))
def fun1[A](value: A)(implicit: fa: TypeClass[A]): A = ...
def fun2[A : TypeClass](value: A): A = ...

useful sources

FP design patterns

function composition

def parseXml(input: String): XmlDocument
def getUserDataNode(document: XmlDocument): XmlNode
def extractUserData(node: XmlNode): MyUserData

val extractUserDataFromInput = parseXml andThen
                               getUserDataNode andThen
                               extractUserData
val handleSome: PartialFunction[Option[String], Unit] = {
  case Some(result) => println(s"Result is $result")
}
val handleNone: PartialFunction[Option[String], Unit] = {
  case None => println(s"There is no result")
}

val handleOption = handleSome orElse handleNone

type classes

trait MyTypeclass[A] {
  def prettyString(printed: A): String
}
implicit def typeclassForMap[K, V] = new MyTypeclass[Map[K, V]] {
  def prettyString(printed: Map[K, V]) =
    printed map { case (k, v) => s"($k -> $v)" } mkString "; "
}
implicit def typeclassForList[B] = new MyTypeclass[List[B]] {
  def prettyString(printed: List[B]) = s"(${printed mkString ", "})"
}
def printMe[A](toPrint: A)(implicit myTypeClass: MyTypeclass[A]) =
  myTypeClass.prettyString(toPrint)
// alternatively
def printMe[A : MyTypeclass](toPrint: A) =
  implicitly[MyTypeclass[A]].prettyString(toPrint)
printMe(List(1, 2, 3)) // (1, 2, 3)
printMe(Map(1 -> 2, 3 -> 4)) // (1 -> 2); (3 -> 4)

semigroup

trait Semigroup[A] {
  def combine(a1: A, a2: A): A
}

implicit def listSemigroup[A] = new Semigroup[List[A]] {
  def combine(a1: List[A], a2: List[A]): List[A] = a1 ++ a2
}

monoid

trait Monoid[A] extends Semigroup[A] {
  def zero: A
}

implicit def listMonoid[A] = new Semigroup[List[A]] {
  def combine(a1: List[A], a2: List[A]): List[A] = a1 ++ a2
  def zero: List[A] = List.empty
}

functor

trait Functor[F[_]] {
  def map[A, B](f: F[A], fun: A => B): F[B]
}

implicit def listFunctor[A] = new Functor[List] {
  def map[A, B](f: List[A], fun: A => B): List[B] = f map fun
}

monad

trait Monad[F[_]] extends Functor[F] {
  def unit[A](a: A): F[A]
  def flatMap[A, B](f: F[A], fun: A => F[B]): F[B]
}

implicit def listFunctor[A] = new Monad[List] {
  def map[A, B](f: List[A], fun: A => B): List[B] = f map fun
  def unit[A](a: A): List[A] = List(a)
  def flatMap[A, B](f: List[A], fun: A => List[B]): List[B] = f flatMap fun
}

other useful concepts

  • applicative functors
  • arrows
  • lenses
  • prisms
  • recursive schemes
  • ...

useful sources

OOP design patterns

abstract factory/factory method

class ConfigFactory {

  MyConfig createConfig(
      Boolean isDebug,
      Set<String>: flags) { ... }
}

new ConfigFactory()
    .createConfig(isDebug, flags);
def createConfig(
    isDebug: Boolean,
    flags: Set[String]): MyConfig = { ... }

createConfig(isDebug, flags)

builder

StringBuilder sb = new StringBuilder();
for (String line : fileContent) {
  sb.append("line: ")
    .append(line)
    .append("\n");
}
String result = sb.toString();
val result =
fileContent.foldLeft(List.empty[String]) {
  case (result, line) =>
    result :+ "line: " :+ line
} mkString "\n"

builder

interface MyConfig {
  Boolean isDebig();
  Set<String> getFlags();
}

class MyConfigBuilder {
  MyConfigBuilder setDebug(
      Boolean isDebug) { ... }
  MyConfigBuilder setFlags(
      Set<String> flags) { ... }
  MyConfig build() { ... }
}

new MyConfigBuilder()
    .setDebug(true)
    .setFlags(flags)
    .build();
case class MyConfig(
    isDebug: Boolean = false,
    flags: Set[String] = Set.empty)

MyConfig().copy(isDebug = true, flags = flags)
// or MyConfig(isDebug = true, flags = flags)
// or MyConfig(isDebug = true).copy(flags = flags)
// etc

lazy initialization

interface X {
  String getResult;
}

class LazyXProxy extends X {
  private X inner = null;

  private X getX() {
    if (inner == null)
      inner = ...;
    return X;
  }

  String getResult() {
    return getX.getResult();
  }
}
trait X {
  def getResult: String
}

lazy val lazyX: X = ...

singleton

class Singleton {
  private static Singleton self = null;

  private Singleton() {}

  public static Singleton get() {
    // this should be synchronized!
    if (self == null)
      self = new Singleton();
    return self;
  }
}
object Singleton

command object

interface Command {
  void execute(target: Data);
}

class UpdateDataCommand extends Command {
  void execute(target: Data) { ... } 
}
trait Command extends (Data => Unit)

case object UpdateData extends Command {
  def apply(target: Data): Unit = { ... }
}

observer

  • futures
  • reactive programming in general
  • import rx._
    
    val a = Var(1); val b = Var(2)
    val c = Rx { a() + b() }
    println(c.now) // 3
    a() = 4
    println(c.now) // 6

decorator

interface X {
  String getResult();
}

class XDecorator extends X {
  private X x;
  XDecorator(X x) { this.x = x; }

  String getResult() {
    return x.getResult();
  }

  String getResultUpper() {
    return getResult().toUpperCase();
  }
}
trait X {
  def getResult: String
}

implicit class XDecorator(x: X) {
  def getResultUpper = x.getResult.toUpperCase
}

template method

abstract class SeriousOperations {
  void doSeriousStuff() {
    // doing things
    String nextResult =
        templateMethod(resultSoFar);
    // doing more things
  }

  abstract String templateMethod(
      String resultSoFar);
}
class SeriousImplementation
    extends SeriousOperations {
  String templateMethod(
      String resultSoFar) { ... }
}
def seriousOperations(
  template: String => String
): Unit = {
  // doing things
  val nextResult = template(resultSoFar);
  // doing more things
}

strategy

interface PrintStrategy {
  String print(Data data);
}
class XmlPrintStrategy
    extends PrintStrategy {
  String print(Data data) { ... }
}
class JsonPrintStrategy
    extends PrintStrategy {
  String print(Data data) { ... }
}
val printXMl: Data => String = ...
val printJson: Data => String = ...

interpreter

sealed trait CalcDSL[T]
object CalcDSL {
  case class AskForInput(question: String) extends CalcDSL[Int]
  case class PrintResult(result: String) extends CalcDSL[Unit]
  case class Add(i: Int, j: Int) extends CalcDSL[Int]
  case class Mul(i: Int, j: Int) extends CalcDSL[Int]

  class Ops {
    def askForInput(question: String): Free[CalcDSL, Int] =
        Free.liftF(AskForInput(question))
    def printResult(result: String): Free[CalcDSL, Unit] =
        Free.liftF(PrintResult(result))
    def add(i: Int, j: Int): Free[CalcDSL, Int] = Free.liftF(Add(i: Int, j: Int))
    def mul(i: Int, j: Int): Free[CalcDSL, Int] = Free.liftF(Mul(i: Int, j: Int))
  }
}

interpreter

val interpreter = new (CalcDSL ~> Id) {
  def apply[A](in: CalcDSL[A]): A = in match {
    case CalcDSL.AskForInput(question) => println(question); StdIn.readInt()
    case CalcDSL.PrintResult(result)   => println(result)
    case CalcDSL.Add(i, j)             => i + j
    case CalcDSL.Mul(i, j)             => i * j
  }
}

interpreter

val ops = new CalcDSL.Ops
val program: Free[CalcDSL, (Int, Int)] = for {
  a <- ops.askForInput("a = ")
  b <- ops.askForInput("b = ")
  c <- ops.add(a, b)
  _ <- ops.printResult(s"a + b = $c")
  d <- ops.mul(a, b)
  _ <- ops.printResult(s"a * b = $d")
} yield (c, d)

val (c, d) = program.foldMap(interpreter)

useful sources

macros and type-level

example of macro generated code

import play.api.libs.json._

case class Resident(name: String, age: Int, role: Option[String])
implicit val residentFormat = Json.format[Resident] // code generated by macro

val resident = Resident(name="Fiver", age=4, role=None)
val residentJson: JsValue = Json.toJson(resident)

val jsonString: JsValue = Json.parse("""{
                                       |  "name" : "Fiver",
                                       |  "age" : 4
                                       |}""".stripMargin)
val residentFromJson: JsResult[Resident] = Json.fromJson[Resident](jsonString)

example of type-level achievements

import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._

sealed trait Foo
case class Bar(xs: List[String]) extends Foo
case class Qux(i: Int, d: Option[Double]) extends Foo

val foo: Foo = Qux(13, Some(14.0))

foo.asJson.noSpaces
// res0: String = {"Qux":{"i":13,"d":14.0}}

decode[Foo](foo.asJson.spaces4)
// res1: Either[io.circe.Error,Foo]

useful sources

don't port (bad) Java practices to Scala

for DI rely on types instead of reflection

bind(classOf[Dependency])
    .to(classOf[DependencyImpl])
class Component @Inject (deps: Dependency) {}
// what if Dependency is abstract
// and you forgot to configure:
// - providers,
// - bindings,
// - etc?

// why not simply?
new Component(dependency)

there are no statements, only expressions

and so we never need to use return

// no need to use var, return
val r1 = if (condition) "some value" else "some other value"

val r2 = for (i <- 0 to 100)
  yield i * 2

def ok      = List(1, 2, 3).foldLeft(0) { _ + _ }         // 6
def invalid = List(1, 2, 3).foldLeft(0) { return _ + _ }  // 1 !!!

there are no statements, only expressions

and so we never need to use return

// no need to use var, return
val r1 = if (condition) "some value" else "some other value"

val r2 = for (i <- 0 to 100)
  yield i * 2

def ok      = List(1, 2, 3).foldLeft(0) { _ + _ }         // 6
def invalid = List(1, 2, 3).foldLeft(0) { return _ + _ }  // 1 !!!

using exception for error handling

def doSomething: Int = 
  if (condition) 1024
  else throw new Exception("invalid result")

val result = Try(doSomething) // we need to handle exception
                              // (and be aware that the function throws it)
def doSomething: Either[String, Int] =
  if (condition) Right(1024)
  else Left("invalid result")

val result = doSomething // type indicate 2 possible results
def doSomething: Validated[String, Int] =
  if (condition) Validated.valid(1024)
  else Validated.invalid("invalid result")

val result = doSomething // possible error indicated by type even better

other offenders

  • deep hierarchies - difference in behavior can injected as functions
  • heavy violations of SOLID/DRY/YAGNI/... - never good
  • using mutable state in not-exceptional cases - a lot of optimizations can be achieved by changing used algorithms and data structures, and making data more CPU-cache friendly
  • using nulls - exception: interop with Java

Summary

  • language and libraries give us greater power of expressions using functions
  • harvesting the whole power of a language/paradigm requires learning some design patterns it uses
  • Java comes with some inherited baggage - in Scala we can avoid some solutions that Java just cannot get rid of

Questions?

Thank you!