Scala as Java++

Workshop - Day 1

Cristian Spinetta

cristian.spinetta@despegar.com

Agenda

  • Sintaxis básica
  • Recursividad
  • Stack-safe mediante tail-recursive
  • Operaciones con listas

Día 1:

  • Case class y Pattern Matching
  • Tipos de Datos Algebraicos (ADT)
  • Effects

Día 2:

Recursos que utilizamos

Ejercicios

Requerimientos:

  • JDK 1.8
  • Scala 2.12

Instalaciones

Instalar Scala para usar el REPL:

https://www.scala-lang.org/download/install.html

Descargar SBT e Intellij IDEA:

https://www.scala-lang.org/download/

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:

Sólo objetos y mensajes

Objetos [en serio]

1 + 2 * 3 / x

Todas las operaciones (+, *, etc.) en realidad son métodos.

Se puede usar casi cualquier identificar como nombre de método.

El compilador hace optimizaciones como cambiar el método +() implementado en un número por la operación de adición de la JVM.

(1).+(((2).*(3))./(x))

en realidad es:

No hay primitivos. Todos los valores son objetos, incluso las funciones.

Todos los valores tienen tipo, incluso las funciones

Tipos unificados

AnyVal representa Tipos de Valores (números, caracteres, etc.). Hay 9 predefinidos.

Unit es un tipo que representa un "valor no significativo". Es singleton y se puede especificar literalmente de la forma ().

AnyRef representa Tipos de Referencias. Hay muchas definidas por la librería estándar de Scala. Cualquier tipo creado en Scala, hereda de AnyRef.

Any es el top-type, y define métodos como equals(), toString(), etc.

Nothing es subtipo de todos los tipos.

Null es un tipo especial, que es subtipo de todos los AnyRef, y sirve para interoperabilidad en la JVM.

Value/Variables

> var score = 100
score: Int = 100

> score = 120
score: Int = 120
> val name = "Alice"
name: String = Alice

> name = "Bob"
error: reassignment to val
       name = "Bob"
            ^
scala> lazy val lazyResult = {
     | println("computing value")
     | 23 }
lazyResult: Int = <lazy>

scala> lazyResult
computing value
res16: Int = 23

scala> lazyResult
res17: Int = 23

Mutable:

Inmutable:

Inicialización lazy:

Se ejecuta en cada invocación:

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

scala> computeResult
computing value
: Int = 10

scala> computeResult
computing value
: Int = 10

En realidad es un método

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.

También se pueden escribir como métodos, con def:

scala> def addOne(x: Int) = x + 1
addOne: (x: Int)Int

Son expresiones que reciben N parámetros. Ej:

scala> val addOne = new Function1[Int, Int] {
     |     override def apply(x: Int): Int = x + 1
     |   }
addOne: Function[Int,Int] = <function1>

Las clases que lo modelan son: Function1[A, B], Function2[A, B, C], ...

El ejemplo anterior usa un syntax-sugar de Scala para definir funciones, que es equivalente a:

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 reutilizarla

Abstracción: permite separar qué hacer Vs. cómo hacerlo

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

Ejercicio

Implementar todos los métodos de: com.despegar.scala.workshop.dia1.BasicSyntax

Estilo funcional

Estilo funcional

- Book: Programming in Scala by Odersky, Spoon, Venners. Chapter 3

Si devuelve Unit, hay un side-effect.

Si hay var, hay mutabilidad.

Cómo reconocer un estilo NO funcional:

Scala te permite programar con un estilo imperativo, pero te anima fuertemente a programar con un estilo funcional

Programación funcional: basado en funciones puras

Sin side-effect

Para el mismo input, el mismo output

Independiente del contexto de ejecución

Estilo funcional

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

Ejemplo:

def printArgs(args: Array[String]): Unit =
  for (arg <- args)
    println(arg)

Sin el var:

def printArgs(args: Array[String]): Unit =
  args.foreach((arg) => println(arg))

O simplemente:

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

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

println() tiene un side-effect.

Podemos separar qué imprimir del cómo:

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
  • Más difícil de seguir
  • 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
  • En general más conciso y fácil de razonar

Qué hacer

Cómo hacerlo

Ejercicios

Implementar:

En com.despegar.scala.workshop.dia1.Factorial

def calculateInt(num: Int): Int
def calculateForBigNumbers(num: Int): Int

Pueden cambiar los tipos de datos

¿Que pasa si hacemos calculateInt(1000)?

Reimplementar eligiendo otro tipo de dato:

¿Que pasa si hacemos calculateForBigNumbers(25000)?

Stack-Unsafe Recursion

// Take the sum of the integers between a and b:
def sumInts(a: Int, b: Int): Int = {
  if (a > b) 0
  else a + sumInts(a + 1, b)
}

La recursividad puede agotar el stack si no corta a tiempo

sumInts(1, 10000) // 50005000
sumInts(1, 100000) // java.lang.StackOverflowError

sumInts() es evaluado como:

sumInts(1, 5)
if (1 > 5) 0 else 1 + sumInts(1 + 1, 5)
if (false) 0 else 1 + sumInts(1 + 1, 5)
1 + sumInts(1 + 1, 5)
1 + sumInts(2, 5)
1 + (2 + sumInts(3, 5))
1 + (2 + (3 + sumInts(4, 5)))
1 + (2 + (3 + (4 + sumInts(5, 5))))
1 + (2 + (3 + (4 + (5 + sumInts(6, 5)))))
1 + (2 + (3 + (4 + (5 + 0))))

Ejemplo:

Stack-Safe Recursion

gcd(14, 21)
if (21 == 0) 14 else gcd(21, 14 % 21)
if (false) 14 else gcd(21, 14 % 21)
gcd(21, 14 % 21)
gcd(21, 14)
gcd(14, 7)
gcd(7, 0)
if (0 == 0) 7 else gcd(0, 7 % 0)
if (true) 7 else gcd(0, 7 % 0)
7

Las cuales son evaluadas como:

¿Cuál es la diferencia entre ambas funciones recursivas?

sumInts() requiere el resultado de la invocación recursiva, para aplicarle una operación (sumarlo con otro valor).

// Sum integers between a and b:
def sumInts(a: Int, b: Int): Int =
  if (a > b) 0
  else a + sumInts(a + 1, b)
// Greatest common divisor of a and b:
def gcd(a: Int, b: Int): Int =
  if (b == 0) a
  else gcd(b, a % b)

Ejemplo:

gcd() no necesita operar con el resultado de la invocación recursiva, simplemente la devuelve como resultado.

Tail-Call

Hay funciones que pueden ser optimizadas para no consumir el stack

sumInts(1, 5)
if (1 > 5) 0 else 1 + sumInts(1 + 1, 5)
if (false) 0 else 1 + sumInts(1 + 1, 5)
1 + sumInts(1 + 1, 5)
1 + sumInts(2, 5)
1 + (2 + sumInts(3, 5))
1 + (2 + (3 + sumInts(4, 5)))
1 + (2 + (3 + (4 + sumInts(5, 5))))
1 + (2 + (3 + (4 + (5 + sumInts(6, 5)))))
1 + (2 + (3 + (4 + (5 + 0))))

Stack-Safe Recursion

Tail recursion: funciones que se llaman a sí misma como última acción.

Si una función es Tail-recursive, el stack frame se puede reutilizar.

Scala detecta las funciones tail-recursive, y las optimiza para reutilizar el stack.

Se le puede pedir a Scala que valide y rompa en compilación, si una función no es tail-recursive, usando el annotation @scala.annotation.tailrec:

// Sum integers between a and b:
@tailrec
def sumInts(a: Int, b: Int): Int =
  if (a > b) 0
  else a + sumInts(a + 1, b)
// Greatest common divisor of a and b:
@tailrec
def gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)

Error de compilación

Compila!!

def sumIntsRec(a: Int, b: Int): Int = {
  @tailrec
  def go(a: Int, acc: Int): Int = {
    if (a > b) acc
    else go(a + 1, acc + a)
  }
  go(a, acc = 0)
}

Compila!!

Ejercicio

Implementar:

En com.despegar.scala.workshop.dia1.Factorial

def calculateRec(num: BigInt): BigInt

¿Que pasa si hacemos calculateRec(1000000000)?

Estructuras de Datos Funcionales

Estructuras de Datos Funcionales

Son inmutables

Las operaciones de escritura devuelven una nueva estructura

Listas

val fruits = List("apples", "oranges", "pears")
val nums   = List(1, 2, 3, 4)
val diag3  = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))
val empty  = List()

Ejemplo:

Es la estructura fundamental en funcional

2 diferencias importantes entre Listas y Arrays:

Las listas son inmutables, los elementos no se pueden cambiar.

Las listas son recursivas, mientras que los arrays son planos.

Listas: estructura

Estructura:

Un List() puede ser:

Una lista vacía: Nil

Un elemento unido a una lista: Cons(head, tail), o bien: head :: tail

Cons("a", Cons("b", Nil))

Ejemplo:

List("a", "b")

Otra forma:

"a" :: "b" :: Nil

O bien:

Es una lista enlazada con estructura recursiva

Listas: estructura

Las operaciones de escrituras, no duplican ningún elemento

Ambas listas comparten los mismos elementos en memoria

No hay necesidad de hacer una copia defensiva de todos los elementos, ya que la lista es inmutable

Pattern Matching con Listas

val list: List[Int] = ???

list match {
  case List(1, 2)    => println("Lista compuesta por los elementos 1 y 2")
  case 1 :: 2 :: Nil => println("Lista compuesta por los elementos 1 y 2")
  case 1 :: 2 :: xs  => println("Lista que comienza con los elementos 1 y 2")
  case x :: y :: xs  => println("Lista con 2 o más elementos")
  case x :: y :: Nil => println("Lista con solo 2 elementos")
  case List()        => println("Lista vacía")
  case Nil           => println("Lista vacía")
  case x :: xs       => println("Lista no vacía")
}

Ejemplo:

Las listas se pueden descomponer usando pattern matching

def sum(ints: List[Int]): Int = ints match {
  case Nil => 0
  case x :: xs => x + sum(xs)
}

Ejemplo de como se usa:

val x = List(1,2,3,4,5) match {
  case x :: 2 :: 4 :: _ => x
  case Nil => 42
  case x :: y :: 3 :: 4 :: _ => x + y
  case h :: t => h + sumInts(t)
  case _ => 101
}

Ejercicio:

¿Cuánto vale x?

3

Listas: operaciones

val fruits = List("apples", "oranges", "pears")
fruits.head == "apples"
fruits.tail.head == "oranges"
fruits.tail == List("oranges", "pears")
fruits.isEmpty == false
Nil.head == throw new NoSuchElementException("head of empty list")

Ejemplo:

Todas las operaciones se pueden definir en término de estas 3:

head

El primer elemento de la lista

tail

La lista compuesta por todos los elementos excepto el primero

isEmpty

true si es vacía, false si tiene elementos

Listas: operaciones

Algunas funciones básicas en las listas:

xs.length

Cantidad de elementos

xs.last

Último elemento de la lista

xs.init

Lista con todos los elementos excepto el último

xs.take(n)

Lista con los primeros n elementos, o xs si la lista es menor

xs.drop(n)

El resto de la lista, después de tomar los n primeros

xs(n)

El elemento en la posición n - 1 (equivalente a xs.apply(n))

xs ++ ys

Lista compuesta por los elementos de xs, seguidos por los de ys

xs.reverse

Lista compuesta por los elementos de xs en posición inversa

xs.updated(n, x)

La misma lista que xs, pero en el index n con el elemento x

xs.indexOf(x)

El index del primer elemento en xs igual a x, o -1 en caso contrario

xs.contains(x)

Equivalente a hacer 'xs.indexOf(x) >= 0'

Listas: operaciones

Función length:

def length[A](xs: List[A]): Int = xs match {
  case Nil          => 0
  case head :: tail => 1 + length(tail)
}

Función last:

def last[A](l: List[A]): A = l match {
  case Nil      => throw new RuntimeException("Bum!")
  case x :: Nil => x
  case _ :: xs  => last(xs)
}

Función init:

def init[A](l: List[A]): List[A] = l match {
  case Nil      => throw new RuntimeException("Bum!")
  case x :: Nil => Nil
  case x :: xs  => x :: init(xs)
}

Ejercicios

def flatten[A](xs: List[List[A]]): List[A]
def dropWhile[A](xs: List[A], f: A => Boolean): List[A]
def concat[A](xs: List[A], ys: List[A]): List[A]
def reverse[A](xs: List[A]): List[A]
def isEmpty[A](xs: List[A]): Boolean

Implementar:

En com.despegar.scala.workshop.dia1.ListUtils

Patrones recurrentes usando listas

Hay 3 patrones que se repiten recurrentemente:

Combinar todos los elementos usando alguna operación

Transformar cada elemento de la lista

Devolver todos los elementos que satisfagan un criterio

Map

Un patrón que aparece recurrentemente es transformar cada elemento de una lista, aplicando una función

scaleList:

ages:

Estas operaciones pueden ser generalizadas con la función map():

def scaleList(xs: List[Double], factor: Double): List[Double] = xs match {
  case Nil => Nil
  case y :: ys => y * factor :: scaleList(ys, factor)
}
def ages(people: List[Person]): List[Int] = people match {
  case Nil => Nil
  case x :: xs => x.age :: ages(xs)
}
def map[A, B](xs: List[A])(f: A => B): List[B]
map(scaleElems)(x => x * factor)
map(ages)(x => x.age)

Ej de uso:

Ejercicios

def map[A, B](xs: List[A])(f : A => B): List[B]
def scaleList_viaMap(xs: List[Double], factor: Double): List[Double]
def ages_viaMap(people: List[Person]): List[Int]
def flatMap[A, B](xs: List[A])(f : A => List[B]): List[B]

Implementar:

En com.despegar.scala.workshop.dia1.ListUtils

Reimplementar usando map:

Filter

Otro patrón recurrente es el de devolver una lista con todos los elementos que satisfacen un criterio.

posElems:

Se puede generalizar implementando la función filter():

def posElems(xs: List[Int]): List[Int] = xs match {
  case Nil => Nil
  case y :: ys => if (y > 0) y :: posElems(ys) else posElems(ys)
}
def filter[A](xs: List[A])(f: A => Boolean): List[A]

adults:

def adults(persons: List[Person]): List[Person] = xs match {
  case Nil => Nil
  case y :: ys => if (y.age >= 18) y :: adults(ys) else adults(ys)
}

Ejercicios

def filter[A](xs: List[A], f: A => Boolean): List[A]
def posElems_viaFilter(xs: List[Int]): List[Int]
def adults_viaFilter(persons: List[Person]): List[Person]

Implementar:

En com.despegar.scala.workshop.dia1.ListUtils

Re implementar usando filter:

Fold

Otro patrón que aparece recurrentemente es combinar todos los elementos con alguna operación.

sum:

length:

concat:

def sum(ints: List[Int]): Int = ints match {
  case Nil     => 0
  case x :: xs => x + sum(xs)
}
def length[A](l: List[A]): Int = l match {
  case Nil     => 0
  case _ :: xs => 1 + length(xs)
}
def concat[A](xs: List[A], ys: List[A]): List[A] = xs match {
  case Nil          => ys
  case head :: tail => head :: concat(tail, ys)
}

flatten:

def flatten[B](xs: List[List[B]]): List[B] = xs match {
  case Nil     => Nil
  case x :: xs => x ++ flatten(xs)
}

Ejercicios

def foldRight[A,B](as: List[A], z: B)(f: (A, B) => B): B
def sum_viaFoldRight(ints: List[Int]): Int
def length_viaFoldRight[A](l: List[A]): Int
def concat_viaFoldRight[A](l1: List[A], l2: List[A]): List[A]
def flatten_viaFoldRight[B](xs: List[List[B]]): List[B]

Implementar:

En com.despegar.scala.workshop.dia1.ListUtils

Re implementar usando foldRight:

def map_viaFoldRight[B](xs: List[Int], f : Int => B): List[B]

foldRight

Aplicar a cada elemento

foldRight:

No es tail-recursive, con lo cual no se puede optimizar para hacerlo stack-safe

def foldRight[A, B](l: List[A], z: B, f: (B, A) => B): B = l match {
  case Nil => z
  case x :: xs => f(x, foldRight(xs, z, f))
}

Análisis de sum(List(1,2,3)) aplicando substitution model:

foldRight(List(1, 2, 3), 0)((x, y) => x + y)
1 + foldRight(List(2, 3), 0)((x, y) => x + y)
1 + (2 + foldRight(List(3), 0)((x, y) => x + y))
1 + (2 + (3 + (foldRight(Nil, 0)((x, y) => x + y))))
1 + (2 + (3 + (0)))
6

Ejercicios

def foldLeft[A,B](as: List[A], z: B)(f: (B, A) => B): B

Tal que sea una versión de foldRight pero stack-safe, usando la técnica de tail-recursive vista.

def sum_viaFoldLeft(ints: List[Int]): Int
def length_viaFoldLeft[A](l: List[A]): Int
def reverse_viaFoldLeft[A](l: List[A]): List[A]

Implementar:

En com.despegar.scala.workshop.dia1.ListUtils

Reimplementar usando foldLeft:

foldLeft

1° intento de foldLeft:

Es tail-recursive!!

def foldLeft[A, B](l: List[A], z: B, f: (A, B) => B): B = {
  @tailrec
  def go(sublist: List[A], acc: B): B = sublist match {
    case Nil => acc
    case x :: xs => go(xs, f(x, acc))
  }
  go(l, z)
}

2° intento de foldLeft:

@tailrec
def foldLeft[A, B](l: List[A], z: B, f: (A, B) => B): B = l match {
  case Nil => z
  case x :: xs => foldLeft(xs, f(x, z), f)
}

Y está generalizado para cualquier tipo de lista y operación!!!

foldLeft Vs. foldRight

sum(List(1,2,3)) mediante foldLeft():

foldLeft(List(1, 2, 3), 0)((x, y) => x + y)
foldLeft(List(2, 3), 0 + 1)((x, y) => x + y)
foldLeft(List(3), (0 + 1) + 2)((x, y) => x + y)
foldLeft(Nil, ((0 + 1) + 2) + 3)((x, y) => x + y)
((0 + 1) + 2) + 3
6

foldLeft es la dualidad de foldRight

sum(List(1,2,3)) mediante foldRight():

foldRight(List(1, 2, 3), 0)((x, y) => x + y)
1 + foldRight(List(2, 3), 0)((x, y) => x + y)
1 + (2 + foldRight(List(3), 0)((x, y) => x + y))
1 + (2 + (3 + (foldRight(Nil, 0)((x, y) => x + y))))
1 + (2 + (3 + (0)))
6

Para operadores asociativos y conmutativos, foldLeft() y foldRight() son equivalentes

foldRight aplica el operador de izquierda a derecha

foldLeft aplica el operador de derecha a izquierda

Ejercicios

def foldRight_viaFoldLeft[A,B](as: List[A], z: B)(f: (A, B) => B): B

Tal que foldRight sea stack-safe

Reimplementar foldRight usando reverse y foldLeft:

En com.despegar.scala.workshop.dia1.ListUtils

Scala as Java++

Workshop - Day 2

Manu Lima| Cristian Spinetta

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:

Manejo de Errores

val unNerd = students.find(_.grade >= 9)

Manejo de errores

null?

throw Exception?

Paso un centinela para que me lo devuelva si no encuentra nada?

Dado los siguientes casos:

¿Qué esperaría que devuelva si no hay ningún nerd?

def mean(xs: List[Double]): Double = xs match {
  case Nil => ???
  case xs => xs.sum / xs.size
}

¿Qué devuelvo si la lista es vacía?

Manejo de errores

Lo bueno y lo malo de usar excepciones:

Fácil de implementar.

Si es chequeada, además de generar mucho código repetitivo del lado de quien lo invoca, no funcionan en Higher-Order Functions.

Introduce la necesidad de que quién lo llame, maneje excepciones.

Si no es checkeada, el tipo de la función no nos dice nada respecto a que puede fallar, y el compilador no nos puede forzar a que la manejemos.

Lo bueno y lo malo de devolver null:

Fácil de implementar.

Requiere de alguna convención o política implícita que todos los que usen la función deben conocer. Y el compilador no nos puede alertar al respecto.

Error-prone: permite que se propague un error silenciosamente.

Genera mucho código repetido del lado de quien lo invoca.

Posiblemente lo más eficiente en uso de memoria y procesamiento.

Errores en funcional

La solución es representarlo explícitamente en el tipo de la respuesta:

Option[T]: Permite modelar la falta de respuesta

Devolver la respuesta o falta de respuesta en un contenedor, y así delegar en quien me invocó la decisión de qué hacer.

Un Option() puede ser:

Falta de respuesta: None

Existe una respuesta: Some(value)

val player1: Option[String] = Some("Alice")
val player2: Option[String] = None

Ejemplo:

Option[T]

Option[Person]

val nerd = students.find(_.grade >= 9)
val nerd: Option[Person] = students.find(_.grade >= 9)

Volviendo a los casos planteados:

def mean(xs: List[Double]): Option[Double] = xs match {
  case Nil => ???
  case _   => ???
}

¿De qué tipo es nerd?

def mean(xs: List[Double]): Option[Double] = xs match {
  case Nil => None
  case _   => Some(xs.sum / xs.length)
}

El tipo de retorno ahora refleja que puede haber un nerd o no.

Delega en quien lo invoca, qué hacer si no hay nerd.

Option[T] es ampliamente utilizado en la librería estandar de Scala, y en general en todas las librerias y apps escritas en Scala

Option[T] y Higher-Order Functions

trait Option[A] {
  def map[B](f: A => B): Option[B]
  def flatMap[B](f: A => Option[B]): Option[B]
  def getOrElse[B >: A](default: => B): B
  // Con => el parámetro no se evalua a menos que se utilice
  def orElse[B >: A](ob: => Option[B]): Option[B]
  def filter(f: A => Boolean): Option[A]
}

El Option se lo puede pensar como una lista que puede contener 1 o 0 elementos.

Todas las higher-order functions implementadas para las listas, pueden ser implementadas en Option[T] :

Ejemplo de uso:

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

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, client: String) 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) =>
    Response(s"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)

Case class:

class ManuallyUser(val id: Int, val name: String) extends Product with Serializable {
  // El compilador agrega varios métodos como: copy(), hashCode(), equals(), etc.
  override def toString: String = s"ManuallyUSer($id,$name)"
}
object ManuallyUser {
  def apply(id: Int, name: String): ManuallyUser =
    new ManuallyUser(id, name)
  // for pattern matching
  def unapply(manuallyUser: ManuallyUser): Option[(Int, String)] =
    Some((manuallyUser.id, manuallyUser.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

ManuallyUser(1, "pepe") match {
  case ManuallyUser(1, "pepe") => "Yes!"
  case x => s"No: $x"
}
// "Yes!"
ManuallyUser(2, "pepe") match {
  case ManuallyUser(1, "pepe") => "Yes!"
  case x => s"No: $x"
}
// "No: ManuallyUser(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

Ejercicio

Implementar el ADT MyOption[+A], compuesto por MyNil y MySome[A], tal que represente:

En com.despegar.scala.workshop.dia2.MyOption

e implementar todos los métodos del trait MyOption

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]
type F[T] = Either[E, T]
type F[T] = List[T]

(Para algún E)

Programa en F que computa un valor T

type F[T] = Try[T]
type F[T] = Future[T]

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

Puede tener un valor T o no

Devuelve un error E o un resultado T

Genera entre 0 y N valores T

Devuelve un valor T o bien una excepción

Ejecución asincrónica. 3 casos posibles: en ejecución, finalizó bien (con un valor T), o finalizó con una excepción.

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"))
import scala.concurrent.ExecutionContext.Implicits._
Future { 2 }
    .filter(x => x % 2 == 0)
    .map(x => x + 10)

Ejemplo de uso con Future[A]:

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

¡ Muchas Gracias !

We are hiring!     

  http://www.despegar.com/sumate/#

  IT Talent Acq: maria.arean@despegar.com 

 

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.

Workshop: Scala as Java++

By Cristian Spinetta

Workshop: Scala as Java++

Workshop: short intro to Scala language.

  • 343