Cristian Spinetta
Software developer.
Ejemplos y ejercicios:
Requerimientos:
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:
> 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
(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()
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:
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...
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
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.
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
Son inmutables
Las operaciones de escritura devuelven una nueva estructura
Ejemplo: Cuenta bancaria
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.
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:
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;
}
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
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
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:
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:
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:
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:
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:
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
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
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.
Verificar si un elemento está contenido en la lista. Comprueba por igualdad.
people.contains(homer)
Ejemplo:
Verificar si la lista contiene al menos un elemento que cumpla la condición.
people.exists(p => p.age > 50)
Ejemplo:
Boolean
Similar al exists(), pero verifica que la condición se cumpla para todos los elementos.
people.forall(p => p.age > 50)
Ejemplo:
Boolean
Ejecuta una función con cada elemento de la lista.
people.foreach(p => println(p))
Ejemplo:
people.foreach(println)
O bien:
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?
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:
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!
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, 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)
}
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")"
// 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
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]
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
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"))
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
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
A short intro to Scala. Special edition adapted for assistants with no experience.