Cristian Spinetta
Software developer.
Cristian Spinetta
cristian.spinetta@despegar.com
Día 1:
Día 2:
Repo con ejercicios:
Requerimientos:
Instalar Scala para usar el REPL:
Descargar SBT e Intellij IDEA:
Clonar el repo:
Todo es una expresión, no hay control de flujo ni sentencias
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
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
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
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.
Jerarquía de clases: https://scala-lang.org/files/archive/spec/2.12/12-the-scala-standard-library.html
> 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
(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()
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:
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...
Implementar todos los métodos de: com.despegar.scala.workshop.dia1.BasicSyntax
- 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
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:
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
side-effect separado de la lógica de negocio
Qué hacer
Cómo hacerlo
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)?
// 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:
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))))
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!!
Implementar:
En com.despegar.scala.workshop.dia1.Factorial
def calculateRec(num: BigInt): BigInt
¿Que pasa si hacemos calculateRec(1000000000)?
Son inmutables
Las operaciones de escritura devuelven una nueva estructura
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.
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
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
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
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
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'
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)
}
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
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
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:
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:
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)
}
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:
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)
}
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]
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
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:
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!!!
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
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
Manu Lima| Cristian Spinetta
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:
val unNerd = students.find(_.grade >= 9)
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?
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.
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[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
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"))
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.
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
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 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
}
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)
}
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")"
// 10 + 2 + 4 = 16
eval(new Sum(
new Number(10),
new Sum(
new Number(2),
new Number(4)))) // 16
Ejemplo:
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?
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:
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:
¿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.
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
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
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
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
Cuando "sospechamos" que un valor puede ser null:
def printResult(result: Result): String = {
if (result != null)
result.toString
else
"Result is null"
}
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}")
}
}
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")
}
}
Lanzar un thread para que se ejecute asincrónicamente:
val thread: Thread = new Thread() {
override def run(): Unit = {
println("Thread Running")
}
}
thread.start()
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"
}
Option[T]: Permite representar la ausencia de respuesta
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
Either[T]: Effect para modelar errores
Es puro (sin side-effect)
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
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")
}
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.
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]:
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()
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
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:
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:
Tour of Scala: Valores de parámetros por 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
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:
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.
By Cristian Spinetta
Workshop: short intro to Scala language.