Scala como Java++

Agenda

  • Sintaxis básica
  • OO Vs Funcional
  • Operaciones con colecciones
  • Codear!

Material utilizado

Repo de soporte

Requerimientos:

  • JDK 1.8
  • Scala 2.12
  • Sbt 1.2

Sintaxis básica

Todo es una expresión, no hay control de flujo ni sentencias

Expresiones

scala> "Hello World!".toUpperCase()
res1: String = HELLO WORLD!
scala> 2 + 2
res2: Int = 4
scala> val msg = if (scala.math.random() >= 0.5) "upper half" else "lower half"
msg: String = lower half
scala> val msg = try {
     |   if (scala.math.random() >= 0.5) throw new RuntimeException("upps")
     |   "Finish ok"
     | } catch {
     |   case exc: Throwable =>
     |     s"Finish with error. Message: ${exc.getMessage}"
     | }
msg: String = Finish with error. Message: upps
scala> val printResult = println("The answer is 42")
The answer is 42
printResult: Unit = ()

Toda expresión tiene un Tipo asociado, que se valida en compile-time.

Toda expresión retorna un valor cuando es evaluada.

El valor Unit representa la ausencia de valor, es similar a void en Java, pero acá es un valor más, que se puede asignar a una variable, comparar con otro valor, etc.

Expresiones de múltiples líneas

Bloques

println({
  val x = 1 + 1
  x + 1
})
// 3

val value = {
  val a = 1
  val b = { val c = 2; val d = 9; c + d }
  a + b
}
if (value > {val a = 1; a * 2 }) {
  println(s"Value $value is greater than 1*2")
  0
} else value
// Value 12 is greater than 1*2

Se puede combinar varias expresiones en una única expresión, encerrándolas entre llaves: {...}

Se lo llama bloque, o en inglés: block o closure.

El resultado de la última expresión, es el resultado de todo el bloque.

Tour of Scala: Basics

Ejemplos:

def / values

> val name = "Alice"
name: String = Alice

> name = "Bob"
error: reassignment to val
       name = "Bob"
            ^

Con val se guarda el resultado de una expresión:

Con def se guarda una expresión:

scala> def computeResult = {
     | println("computing value")
     | 10 }
computeResult: Int

scala> computeResult
computing value
: Int = 10

scala> computeResult
computing value
: Int = 10

Se puede guardar una referencia a una expresión, o al resultado de la misma

En realidad es un método

Es una variable inmutable, el equivalente a una variable final en Java

Funciones

(x: Int) => x + 1

Tour of Scala: Basics

"ciudadanos de primera clase", se pueden asignar a una variable:

scala> val addOne: Int => Int = (x: Int) => x + 1
addOne: Int => Int = $$Lambda$1648/859538443@5ef3674d

scala> addOne.apply(3)
res105: Int = 4

scala> addOne(3)
res106: Int = 4

A la izquierda del '=>' van los parámetros, y en la derecha la expresión.

Son expresiones que reciben N parámetros. Ej:

Syntax-Sugar: Cualquier método que se llame apply se puede llamar directamente, sin escribir .apply()

Funciones de Orden Superior

import java.time.LocalDateTime

def log(msg: String, writer: String => Unit) = writer(s"${LocalDateTime.now()} : $msg")

log("log line", msg => println(msg)) // 2018-11-01T15:20:56.570 : log line

Reciben a otra:

Tour of Scala: Higher Order Functions

O bien, devuelven como resultado otra función:

def add(x: Int): Int => Int = (y: Int) => x + y

val addTwo: Int => Int = add(2) // a function that add 2 to the given number

addTwo(4)  // 6
addTwo(8)  // 10
addTwo(20) // 22

Composición: permite extraer lógica y combinarla de diferentes maneras

Abstracción: permite ocultar algunos detalles. Ej: qué hacer en cada iteración Vs. cómo iterar

Ventajas:

Dado que las funciones son ciudadanos de primera clase, se pueden pasar como parámetro, o devolver como resultado de una expresión.

Las funciones de Orden Superior son aquellas que:

Pattern Matching

Es una forma generalizada del switch de C o Java. Permite comparar una lista de patrones (case 'patron' =>)

También hay muchas otras formas de armar patrones, usando objetos extractores

def matchTest(x: Int): String = x match {
  case 1 => "one"
  case 2 => "two"
  case 3 | 4 => "three or four"
  case _ => "many"
}

Ejemplo:

def matchTest(x: Int): String = x match {
  case 1 => "one"
  case 2 => "two"
  case 3 | 4 => "three or four"
  case x if x < 1 => s"It's < 1: $x"
  case x => s"It's > 4: $x"
}

Los patrones pueden ser literales, variables (si empieza con minúscula) y pueden tener "guardas":

A lo largo del workshop vamos a ver otras formas de uso...

Estilo funcional

def sort(xs:  Array[Int])  {
  def swap(i: Int,  j: Int){
    val t = xs(i); xs(i) = xs(j); xs(j) = t
  }
  def sort1(l: Int, r: Int)  {
    val pivot = xs((l + r) / 2)
    var i = l; var j = r
    while(i <= j) {
      while(xs(i) < pivot) i += 1
      while(xs(j) > pivot) j -= 1
      if (i <= j) {
        swap(i, j)
        i += 1
        j -= 1
      }
    }
    if(l < j) sort1(l, j)
    if(j  <  r) sort1(i, r)}
  sort1(0, xs.length - 1)
}
def sort(xs: Array[Int]): Array[Int] = {
  if (xs.length <= 1) xs
  else {
    val pivot = xs(xs.length / 2)
    Array.concat(
      sort(xs  filter  (pivot  >)),
           xs  filter  (pivot  ==),
      sort(xs  filter  (pivot  <)))
  }
}

Imperativo

Funcional

Versátil y expresivo

FP + OO

Ejemplo: Quick Sort

Todas las operaciones están implementadas en la librería standard de Scala.

No hay soporte especial del compilador.

Scala permite programar de forma imperativa/objetosa o bien con un estilo funcional.

Estilo funcional

def printArgs(args: Array[String]): Unit = {
  var i = 0
  while (i < args.length) {
    println(args(i))
    i += 1
  }
}

Imperativo:

def formatArgs(args: Array[String]): String =
  args.mkString("\n")

def printArgs(args: Array[String]): Unit =
  println(formatArgs(args))

Funcional:

El side-effect es parte de la lógica de negocio

  • Hace foco en cómo se va a ejecutar
  • Se mezcla que quiero hacer con como lo hago
  • En general código más performante

side-effect separado de la lógica de negocio

  • Separa qué hacer del cómo hacerlo
  • La lógica tiene Transparencia Referencial
  • Razonamiento local
  • En general más conciso y fácil de razonar

Qué hacer

Cómo hacerlo

Estructuras de Datos Funcionales

Son inmutables

Las operaciones de escritura devuelven una nueva estructura

Ejemplo: Cuenta bancaria

Inmutabilidad

class BankAccount {
  var balance = 0

  def deposit(amount: Int) {
    if (amount > 0) balance += amount
    else balance
  }

  def withdraw(amount: Int): Int =
    if (amount >= 0 && amount <= balance) {
      balance -= amount
      balance
    } else {
      println("insufficient funds")
      balance
    }
}

Con mutabilidad

val account = new BankAccount
account.deposit(30) // return 30
account.deposit(20) // return 20
account.withdraw(10) // return 10

// Current balance: 40
case class BankAccount(balance: Int) {

  def deposit(amount: Int) =
    if (amount > 0)
      BankAccount(balance = balance + amount)
    else
      this

  def withdraw(amount: Int) =
    if (amount >= 0 && amount <= balance) {
      BankAccount(balance = balance - amount)
    } else {
      println("insufficient funds")
      this
    }
}

Con inmutabilidad

BankAccount(0)
  .deposit(30) // return a new BankAccount
  .deposit(20) // return a new one
  .withdraw(10) // return a new one

// Balance in the last BankAccount: 40

Inmutabilidad: No se cambia el valor, se crea uno nuevo.

Colecciones

Colecciones

Son inmutables

Las operaciones de escritura generan una nueva estructura

Traversable(1, 2, 3)
Iterable("x", "y", "z")
Map("x" -> 24, "y" -> 25, "z" -> 26)
Set(Color.red, Color.green, Color.blue)
SortedSet("hello", "world")
Buffer(x, y, z)
IndexedSeq(1.0, 2.0)
LinearSeq(a, b, c)

Cada colección se puede crear mediante su nombre:

def isEmpty: Boolean
def size: Int
def ++[B >: A, That](xs: GenTraversableOnce[B])(implicit bf: ...): That
def map[B, That](f: A => B)(implicit bf: ...): That
def filter(p: A => Boolean): Traversable[A]
def groupBy[K](f: A => K): Map[K, Traversable[A]]
def foreach[U](f: A => U): Unit
def forall(p: A => Boolean): Boolean
def exists(p: A => Boolean): Boolean
def count(p: A => Boolean): Int
// Y muchos más...

Traversable agrega a todas las colecciones la mayoría de los métodos que usamos:

Operaciones con colecciones

List<Person> students = new ArrayList<Person>(new Person("Juan", 9),
                                              new Person("Pepe", 6));
List<Person> getNerds(List<Person> students) {

    List<Person> nerds = new ArrayList<>();
    for (Person student : students) {
        if (student.getGrade >= 9) {
            nerds.add(student);
        }
    } 
    return nerds;
}

Imperativo Vs Funcional

Veamos el siguiente ejemplo en Java 1.7:

Lo mismo en Scala:

val students = List(Person("Juan", 9), Person("Pepe", 6))

def getNerds(students: List[Person]) = students.filter(std => std.grade >= 9)

+ Declarativo

+ Expresivo

Filter

people.filter(person => person.name == "Homero Simpson")
def isHomero(person: Person): Boolean = person.name == "Homero Simpson"

people.filter(isHomero)

Ejemplo:

Obtener una nueva lista con los elementosque cumplen una condición

Filter Not

people.filterNot(person => person.height == "Grande")
def isCuca(person: Person): Boolean = person.height == "Grande"
people.filterNot(isCuca)

Ejemplo:

Obtener una nueva lista con los elementosque NO cumplen una condición.

people.filter(!isCuca)

Es lo mismo que:

Map

Obtener una nueva lista con el resultado de aplicar a cada elemento una transformación.

people.map(person => person.pet)
def getPet(person: Person): Pet = person.pet

people.map(getPet)

Ejemplo:

Collect

Se aplica una transformación a los elementos que cumplen una condición, los demás se descartan (parece un filter + map).

persons.collect {
 case p: Person if p.name == "Homero Simpson" => new Payaso()
}

Ejemplo:

Flatten

Aplanar una lista de listas, a una sola lista con los elementos de las sublistas.

type Cat = String
val manyCatBags: List[List[Cat]] = List(List("Snowball 1"),List("Snowball 2"))
val catBags: List[Cat] = manyCatBags.flatten

Ejemplo:

Fold Left

Operación de reducción. Dado un valor inicial (semilla), se le aplica una operación en conjunto con el primer elemento de la lista, el resultado es la "semilla" que se usa con el siguiente elemento, y finalmente en el resultado.

List(1,2,3,4,5).foldLeft(0)((subtotal, i) => subtotal + i)

Ejemplo:

Fold Left

Ejemplo: Homero come Donuts.

case class Donut(fat: Int)

case class Person(weight: Int) {
  def eat(donut: Donut):Person =
    Person(weight + donut.fat)
}

Modelo

val homer = Person(weight = 100)
val donuts = List(Donut(fat = 10),
                  Donut(fat = 20),
                  Donut(fat = 300))

Cargar valores

// Fold left
donuts.foldLeft(homer)((homerAcc, donut) =>
                        homerAcc.eat(donut))
// res0: Person = Person(430)

// Fold right
donuts.foldRight(homer)((donut, homerAcc) =>
                         homerAcc.eat(donut))
// res1: Person = Person(430)

Hacer que Homero coma

Fold Left / Fold Right

Ejemplos:

List(1,2,3,4).foldLeft("Los números son: ")((acc, num) => acc + num.toString)
// res0: String = Los números son: 1234

List(1,2,3,4).foldRight("Los números son: ")((num, acc) => acc + num.toString)
// res1: String = Los números son: 4321

foldLeft recorre de izquierda a derecha

foldRight recorre de derecha a izquierda

Reduce

Caso particular del fold donde la semilla es el primer elemento. Por lo tanto, el resultado es del mismo tipo que los elementos de la lista.

List(1,2,3,4,5).reduce((subtotal, i) => subtotal + i)

Ejemplo:

Warning: si la lista está vacía, se lanza una excepción.

Contains

Verificar si un elemento está contenido en la lista. Comprueba por igualdad.

people.contains(homer)

Ejemplo:

Exists

Verificar si la lista contiene al menos un elemento que cumpla la condición.

people.exists(p => p.age > 50)

Ejemplo:

Boolean

Forall

Similar al exists(), pero verifica que la condición se cumpla para todos los elementos.

people.forall(p => p.age > 50)

Ejemplo:

Boolean

Foreach

Ejecuta una función con cada elemento de la lista.

people.foreach(p => println(p))

Ejemplo:

people.foreach(println)

O bien:

Find

Devuelve el primer elemento que cumpla una condición.

people.find(p => isHomer(p))

Ejemplo:

?

¿Qué pasa si no hay elemento que cumpla la condición?

Effect

Tipo de datos con info extra

Option[T]: Effect para modelar null

¿De qué tipo es homer?

Volviendo al find()...

Option[Person]

people
  .find(person => isHomer(person))
  .map(homer => Response.Ok(homer.name))
  .getOrElse(Response.NotFound("Homer was missed"))
val homer = people.find(person => isHomer(person))
val homer: Option[Person] = people.find(person => isHomer(person))

Ejemplo de uso:

Ejemplo:

Ejercicio

Implementar los métodos del repo que están pendientes (identificados con un ???)

En *

¡ Muchas Gracias !

Ariel Rabinovich -> ariel.rabinovich@despegar.com

Cristian Spinetta -> cristian.spinetta@despegar.com

Bonus!

Construir nuevos Tipos de Datos

Clases

class Greeter(prefix: String, suffix: String) {
  def greet(name: String): Unit =
    println(prefix + name + suffix)
}

Tour of Scala: Classes

val greeter = new Greeter("Hello, ", "!")
greeter.greet("Scala developer") // Hello, Scala developer!

Se define con la keyword class, seguido por un nombre y los parámetros del constructor:

Se puede instanciar con la keyword new:

La lógica del constructor puede estar en cualquier parte del body.

El constructor principal está en la definición de la clase.

Puede ser abstracta, agregando el modificar abstract delante de class.

Los métodos y variables pueden ser private, protected o public (es el default).

Las variables tienen Getter y Setter autogenerados.

Traits

trait Size {
  def size: Int
}

Tour of Scala: Traits

trait Color {
  def color: String = "red"
}

Son Tipos que pueden contener métodos y valores:

Pueden tener implementaciones por default:

Se pueden mixear varios traits en una misma clase:

class UIPoint(x: Int, y: Int) extends Point(x, y)
  with Color
  with Size {
  
  def size: Int = 24
}

Los traits permiten herencia múltiple.

"similar" a interfaces de Java 8

Objects

package logging

object Logger {
  def info(message: String): Unit =
    println(s"INFO: $message")
}

Tour of Scala: Singleton Objects

Logger.info("Hello Scala devs!")
// Prints "INFO: Hello Scala devs!"

Son singleton, clases que tienen una única instancia:

Se puede acceder directamente usando su nombre:

O importando sus métodos:

import logging.Logger.info

class Project(name: String, daysToComplete: Int)

class Test {
  val project1 = new Project("TPS Reports", 1)
  val project2 = new Project("Website redesign", 5)
  info("Created projects")  // Prints "INFO: Created projects"
}

Case classes

case class Point(x: Int, y: Int)

Tour of Scala: Case classes

val point = Point(1, 2)
val anotherPoint = Point(1, 2)
val yetAnotherPoint = Point(1, 3)

Scala tiene clases especiales llamadas case class:

Se puede instanciar sin new, invocando el constructor como una función:

Por default son inmutables y se comparan "por valor":

point == anotherPoint // true

point == yetAnotherPoint // false

Y son fácilmente clonables:

scala> val newPoint = yetAnotherPoint.copy(y = 2)
newPoint: Point = Point(1,2)

También existe la versión singleton:

case object PointOrigin {
  val x: Int = 0
  val y: Int = 0
}

Case class y Pattern Matching

sealed trait Error {
  def msg: String
}
case class ConnectionError(msg: String, clientType: ClientType) extends Error
case class BusinessError(msg: String) extends Error
case class UnrecoverableError(msg: String) extends Error

Tour of Scala: Case classes

Ejemplo:

Los case class generan un patrón a través de su constructor

case class Response(body: String, status: Int)

def errorHandling(error: Error): Response = error match {
  case ConnectionError(msg, client, HttpClient) =>
    Response(s"Rest Client Error trying to connect to $client. Cause: $msg", status = 500)

  case ConnectionError(msg, client, DBClient) =>
    Response(s"DB Error trying to connect to $client. Cause: $msg", status = 500)

  case BusinessError(msg) =>
    Response(s"Wrong requests due to: $msg", status = 400)

  case UnrecoverableError(msg) =>
    Response(s"Unexpected error: $msg", status = 500)

}

Unapply()

case class User(id: Int, name: String)

La siguiente Case Class:

class User(val id: Int, val name: String) extends Product with Serializable {

  // El compilador agrega varios métodos como: copy(), hashCode(), equals(), etc.

  // Ej:
  override def toString: String = s"User($id,$name)"
}

object User {
  def apply(id: Int, name: String): User =
    new User(id, name)

  // for pattern matching
  def unapply(user: User): Option[(Int, String)] =
    Some((user.id, user.name))
}

Genera "más o menos" lo siguiente:

El método unapply permite evaluar si el valor a comparar se puede convertir al case dado:

User(1, "pepe") match {
  case User(1, "pepe") => "Yes!"
  case x               => s"No: $x"
}
// "Yes!"
User(2, "pepe") match {
  case User(1, "pepe") => "Yes!"
  case x               => s"No: $x"
}
// "No: User(2, "pepe")"

Ejercicio: intérprete aritmético

  • Hacer un interpretador de expresiones aritméticas, tal que permita evaluarla.
  • Por simplicidad, nos limitaremos a expresiones compuestas por sumas y números.
  • Las expresiones pueden ser representadas por la siguiente jerarquía de clases: un Trait Expr, y 2 subclases Num y Sum.
// 10 + 2 + 4 = 16
eval(new Sum(
  new Number(10),
  new Sum(
    new Number(2),
    new Number(4)))) // 16

Ejemplo:

Ejercicio: intérprete aritmético

abstract class Expr {
  def isNumber: Boolean
  def isSum: Boolean
  def numValue: Int
  def leftOp: Expr
  def rightOp: Expr
}

class Number(n: Int) extends Expr {
  def isNumber: Boolean = true
  def isSum: Boolean = false
  def numValue: Int = n
  def leftOp: Expr = error("Number.leftOp")
  def rightOp: Expr = error("Number.rightOp")
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def isNumber: Boolean = false
  def isSum: Boolean = true
  def numValue: Int = error("Sum.numValue")
  def leftOp: Expr = e1
  def rightOp: Expr = e2
}

1° intento, con un enfoque imperativo:

def eval(e: Expr): Int = {
  if (e.isNumber) e.numValue
  else if (e.isSum) eval(e.leftOp) + eval(e.rightOp)
  else error("unrecognized expression kind")
}

Problema: es tedioso de mantener.

Para extender los Tipos de Expresión, hay que agregar y modificar muchos métodos.

// 10 + 2 + 4 = 16
eval(new Sum(
  new Number(10),
  new Sum(
    new Number(2),
    new Number(4)))) // 16

Prueba:

¿Cuántos cambios hay que hacer para agregar la multiplicación?

Ejercicio: intérprete aritmético

abstract class Expr {
  def eval: Int
}
class Number(n: Int) extends Expr {
  def eval: Int = n
}
class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

2° intento, con un enfoque puramente objetoso:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

Mucho mejor!

Y ahora si quisieramos agregar la expresión Multiplicación, debería ser simplemente agregar una clase más:

Ejercicio: intérprete aritmético

def prettyPrint(e: Expr): String = {
  if (e.isNumber)   e.numValue.toString
  else if (e.isSum) s"${prettyPrint(e.leftOp)} + ${prettyPrint(e.rightOp)}"
  else error("unrecognized expression kind")
}

prettyPrint(new Sum(new Number(3), new Number(4))) // "3 + 4"

En la 1° solución, sería agregar el siguiente método:

Ahora bien, que pasa si queremos agregar un método que imprima la expresión?

abstract class Expr {
  def eval: Int
  def prettyPrint: String
}
class Number(n: Int) extends Expr {
  def eval: Int = n
  def prettyPrint: String = n.toString
}
class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
  def prettyPrint: String = s"${e1.prettyPrint} + ${e2.prettyPrint}"
}

En la 2° solución, hay que modificar cada clase:

Ejercicio: intérprete aritmético

  • La 1° solución extiende bien en cantidad de operaciones.
  • La 2° solución extiende bien en cantidad de Tipos de Expresiones.

¿Cómo podemos generar una solución que permita extender bien en las 2 direcciones?

Conclusión:

La problemática de analizar cómo extender en 2 dimensiones (agregando Tipos y agregando operaciones) introduciendo la menor cantidad de cambios sobre el código existente, es conocido como expression problem.

Ejercicio: intérprete aritmético

3° intento, enfoque funcional usando case classes y pattern matching:

sealed abstract class Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Evaluación:

def eval(e: Expr): Int = e match {
  case Number(n) => n
  case Sum(l, r) => eval(l) + eval(r)
}

Prueba:

// 10 + 2 + 4 = 16
eval(Sum(
  Number(10),
  Sum(
    Number(2),
    Number(4)))) // 16

Ejercicio: intérprete aritmético

Extensibilidad de la 3° solución:

Agregar operación prettyPrint:

def prettyPrint(e: Expr): String = e match {
  case Number(n) => n.toString
  case Sum(l, r) => s"${prettyPrint(l)} + ${prettyPrint(r)}"
}

Agregar Tipo de expresión Multiplicación:

case class Prod(e1: Expr, e2: Expr) extends Expr

def eval(e: Expr): Int = e match {
  case Number(n)  => n
  case Sum(l, r)  => eval(l) + eval(r)
  case Prod(l, r) => eval(l) * eval(r)
}

def prettyPrint(e: Expr): String = e match {
  case Number(n)  => n.toString
  case Sum(l, r)  => s"(${prettyPrint(l)} + ${prettyPrint(r)})"
  case Prod(l, r) => s"(${prettyPrint(l)} * ${prettyPrint(r)})"
}

Para agregar nuevas operaciones, sólo requiere agregar nuevas funciones.

Para agregar un nuevo Tipo, se requiere agregar un nuevo case y actualizar los Pattern Matching

Algebraic Data Types

Algebraic Data Types

Tipo de Dato compuesto por 1 o más constructores de datos

Ejemplos:

sealed abstract class Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
sealed trait Error {
  def msg: String
}
case class ConnectionError(msg: String, client: String) extends Error
case class BusinessError(msg: String) extends Error
case class UnrecoverableError(msg: String) extends Error

Side-Effect

Manejo de null

Cuando "sospechamos" que un valor puede ser null:

def printResult(result: Result): String = {
  if (result != null)
    result.toString
  else
    "Result is null"
}

Manejo de error

Tipicamente se lanza una excepción:

def divide(dividend: Int, divisor: Int): Int = {
  if (divisor == 0)
    throw new RuntimeException("Divisor can't be 0")
  dividend / divisor
}
def usingDivision: Unit = {
  try {
    val result = divide(10, 0)
    println(s"The result is $result")
  } catch {
    case exc: Throwable =>
      println(s"Exception: ${exc.getMessage}")
  }
}

Manejo de excepciones

Cuando usamos una librería que puede tirar excepción:

def usingDivision: Response = {
  try {
    val result = restTemplate.getForEntity(s"$url/$id", classOf[String])
    Response(status = 200, result)
  } catch {
    case exc: Throwable =>
      println(s"Exception: ${exc.getMessage}")
      Response(status = 500, "Please, retry later")
  }
}

Computación asincrónica

Lanzar un thread para que se ejecute asincrónicamente:

val thread: Thread = new Thread() {
  override def run(): Unit = {
    println("Thread Running")
  }
}
thread.start()

A lo Scala...

Manejo de null

def printResult(result: Result): String = {
  if (result != null)
    result.toString
  else
    "Result is null"
}

Enfoque imperativo

Enfoque funcional

def printResult(foo: Option[Result]): String = {
  foo.map { fooValue =>
    fooValue.value
  } getOrElse "foo is None"
}
  • El compilador no me advierte del posible null
  • Depende del programador/la documentación
  • Si te olvidas, aparecen NullPointerException

Option[T]: Permite representar la ausencia de respuesta

  • Es explicito
  • El compilador puede validarlo
  • Nos obliga a manejar el caso de None
  • Menos código duplicado

Manejo de error

def divide(dividend: Int, divisor: Int):Int = {
  if (divisor == 0)
    throw new RuntimeException(
      "Divisor can't be 0")
  dividend / divisor
}

Enfoque imperativo

Enfoque funcional

def divide(dividend: Int,
           divisor: Int):
    Either[String, Int] = {
  if (divisor == 0)
    Left("Divisor can't be 0")
  else
    Right(dividend / divisor)
}

Genera un side-effect

  • Pierdo Transparencia Referencial
  • Es dependiente del contexto
  • Requiere manejo "especial" por quien lo usa

Either[T]: Effect para modelar errores

Es puro (sin side-effect)

  • Tiene Transparencia Referencial
  • Tipado
  • Separación de responsabilidades

Manejo de excepciones

def usingDivision: Response = {
  try {
    val result = restTemplate
      .getForEntity(s"$url/$id", classOf[String])
    Response(status = 200, result)
  } catch {
    case exc: Throwable =>
      println(s"Exception: ${exc.getMessage}")
      Response(status = 500,
               body = "Please, retry later")
  }
}

Enfoque imperativo

Enfoque funcional

def retrieveValue(): Try[String] = Try {
  restTemplate.getForEntity(s"$url/$id",
                            classOf[String])
}

def getResponse: Response = retrieveValue() match {
  case Success (result) => Response(status = 200, result)
  case Failure (exc)    => Response(status = 500,
                                    body = errorMsg)
}

Try[T]: representa un resultado que puede terminar con excepción

Computación asincrónica

val thread: Thread = new Thread() {
  override def run(): Unit = {
    println("Thread Running")
  }
}
thread.start()

Enfoque imperativo

Enfoque funcional

implicit val executionContext: ExecutionContext = ???

Future {
  println("Thread Running")
}

Future[T]: Representar ejecución asincrónica

Fácil de componer

implicit val executionContext: ExecutionContext = ???

val usdQuote = Future { connection.getCurrentValue(USD) }
val arsQuote = Future { connection.getCurrentValue(ARS) }

val purchase = usdQuote flatMap { usd =>
    arsQuote
      .filter(ars => isProfitable(usd, ars))
      .map(ars => connection.buy(amount, ars))
}

purchase foreach { _ =>
  println("Purchased " + amount + " ARS")
}

Effect

Tiene la forma F[T]

type F[T] = Option[T]

Puede tener un valor T o no

type F[T] = Either[E, T]

Devuelve un error E o un resultado T

type F[T] = List[T]

Genera entre 0 y N valores T

(Para algún E)

Programa en F que computa un valor T

type F[T] = Try[T]

Devuelve un valor T o bien una excepción

type F[T] = Future[T]

Ejecución asincrónica. 3 casos posibles: está en ejecución, o finalizó y tiene un valor T, o finalizó y tiene una excepción

Todos estos Effects, y más, están definidos en la librería estándar de Scala, y traen los métodos map(), flatMap(), flatten, fold(), entre otros...

Tipos de datos con info extra

Effects y Higher-Order Functions

trait F[A] {
  def map[B](f: A => B): F[B]
  def flatMap[B](f: A => F[B]): F[B]
  def getOrElse[B >: A](default: => B): B
  def orElse[B >: A](ob: => F[B]): F[B]
  def filter(f: A => Boolean): F[A]
  // y muchos más!!
}

object F {
  def apply(value: => A): F[A]
}

La mayoría de las higher-order functions implementadas para las listas, se pueden usar en un Effect F[T] :

Ejemplo de uso con Option[A]:

students
  .find(_.grade >= 9)
  .map(nerd => Response.Ok(nerd.name))
  .getOrElse(Response.NotFound("no nerd found"))

For-Comprehension

def getFirstName(user: User): Option[String] = ???
def getLastName(user: User): Option[String] = ???

def getFullName(user: User): Option[String] = {
  getFirstName(user).flatMap { firstName =>
    getLastName(user).map(lastName =>
      s"$firstName:$lastName")
  }
}

Como es tan común en Scala encadenar flatMap + filter + map, existe un syntax-sugar que permite escribirlo de una forma más limpia

def getFullName_viaForCmp(user: User): Option[String] = {
  for {
    firstName <- getFirstName(user)
    lastName  <- getLastName(user)
  } yield {
    s"$firstName:$lastName"
  }
}

Ejemplo:

Escrito con for-comprehension:

Cada linea del for, es una invocación a flatMap(), excepto el último, que es un map()

Ejercicio

Implementar los servicios de usuario

En com.despegar.scala.workshop.dia2.UserService

Métodos

def add(x: Int, y: Int): Int = x + y

Se definen con la palabra clave def:

Tour of Scala: Basics

Pueden tener varias listas de parámetros:

def addThenMultiply(x: Int, y: Int)(multiplier: Int): Int = (x + y) * multiplier

println(addThenMultiply(1, 2)(3)) // 9
def name: String = System.getProperty("user.name")

println(name) // cspinetta

Pueden tener expresiones de multiples líneas:

def getSquareString(input: Double): String = {
  val square = input * input
  square.toString
}

La última expresión es el valor que retorna el método

O ninguna:

Métodos

def area(width: Int = 1, height: Int = 2): Int = width * height

area() // 2

area(10) // 20

area(height = 3) // 3

Los parámetros pueden tener default:

def max(a: Int, b: Int, c: Int): Int = {
  def max(x: Int, y: Int) = if (x > y) x else y
  max(a, max(b, c))
}

Puede haber métodos anidados:

Parámetros by-name

Métodos

def area(width: Int, height: => Int): Int = {
  if(width == 0) 0 else width * height
}

Tour of Scala: By-name parameters

def computeHeight: Int = {
  println("Fetching height...")
  10
}

println(area(1, computeHeight))
// Fetching height...
// 10

println(area(0, computeHeight))
// 0

Call-by-value

Evaluación estricta.

Es el default de Scala (al igual que Java).

Tiene la ventaja de que solo se evalúa 1 vez.

Call-by-name

Lazy: se evalúa solo cuando se usa.

Se marca con un '=>' antes del tipo.

Tiene la ventaja de que si no se usa, no se evalúa.

Método con parámetro by-name:

Ejemplo de uso:

Companion Objects

import scala.math._

case class Circle(radius: Double) {
  import Circle._
  def area: Double = calculateArea(radius)
}

object Circle {
  private def calculateArea(radius: Double): Double =
    Pi * pow(radius, 2.0)
}

val circle = new Circle(5.0)
circle.area

Tour of Scala: Singleton Objects

Un objeto con el mismo nombre que una clase:

Es el equivalente a tener métodos/variables estáticas en una clase.

Un companion puede acceder a los variables/métodos privados de su clase/object.

Tienen que estar en el mismo archivo.

Se suele usar para hacer factories de la clase.

Las variables tienen Getter y Setter autogenerados.

Made with Slides.com