Workshop: Intro to Scala

Cristian Spinetta

cristian.spinetta@despegar.com

(short edition)

Material utilizado

Repo de soporte

Requerimientos:

  • JDK 1.8
  • Scala 2.12

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 combinarla de diferentes maneras

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

Ventajas:

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

Las funciones de Orden Superior son aquellas que:

Pattern Matching

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

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

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

Ejemplo:

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

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

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

Estilo funcional

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

Imperativo

Funcional

Versátil y expresivo

FP + OO

Ejemplo: Quick Sort

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

No hay soporte especial del compilador.

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

Estilo funcional

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

Imperativo:

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

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

Funcional:

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

  • Hace foco en cómo se va a ejecutar
  • 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

Estructuras de Datos Funcionales

Son inmutables

Las operaciones de escritura devuelven una nueva estructura

Ejemplo: Cuenta bancaria

Inmutabilidad

class BankAccount {
  var balance = 0

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

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

Con mutabilidad

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

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

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

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

Con inmutabilidad

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

// Balance in the last BankAccount: 40

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

Colecciones

Colecciones

Son inmutables

Las operaciones de escritura generan una nueva estructura

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

Cada colección se puede crear mediante su nombre:

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

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

Construir nuevos Tipos de Datos

Clases

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

Tour of Scala: Classes

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

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

Se puede instanciar con la keyword new:

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

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

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

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

Las variables tienen Getter y Setter autogenerados.

Traits

trait Size {
  def size: Int
}

Tour of Scala: Traits

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

Son Tipos que pueden contener métodos y valores:

Pueden tener implementaciones por default:

Se pueden mixear varios traits en una misma clase:

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

Los traits permiten herencia múltiple.

"similar" a interfaces de Java 8

Objects

package logging

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

Tour of Scala: Singleton Objects

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

Son singleton, clases que tienen una única instancia:

Se puede acceder directamente usando su nombre:

O importando sus métodos:

import logging.Logger.info

class Project(name: String, daysToComplete: Int)

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

Case classes

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

Tour of Scala: Case classes

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

Scala tiene clases especiales llamadas case class:

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

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

point == anotherPoint // true

point == yetAnotherPoint // false

Y son fácilmente clonables:

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

También existe la versión singleton:

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

Case class y Pattern Matching

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

Tour of Scala: Case classes

Ejemplo:

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

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

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

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

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

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

}

Unapply()

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

La siguiente Case Class:

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

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

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

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

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

Genera "más o menos" lo siguiente:

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

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

Ejercicio: intérprete aritmético

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

Ejemplo:

Ejercicio: intérprete aritmético

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

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

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

1° intento, con un enfoque imperativo:

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

Problema: es tedioso de mantener.

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

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

Prueba:

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

Ejercicio: intérprete aritmético

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

2° intento, con un enfoque puramente objetoso:

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

Mucho mejor!

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

Ejercicio: intérprete aritmético

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

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

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

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

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

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

Ejercicio: intérprete aritmético

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

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

Conclusión:

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

Ejercicio: intérprete aritmético

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

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

Evaluación:

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

Prueba:

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

Ejercicio: intérprete aritmético

Extensibilidad de la 3° solución:

Agregar operación prettyPrint:

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

Agregar Tipo de expresión Multiplicación:

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

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

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

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

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

Algebraic Data Types

Algebraic Data Types

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

Ejemplos:

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

Side-Effect

Manejo de null

Cuando "sospechamos" que un valor puede ser null:

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

Manejo de error

Tipicamente se lanza una excepción:

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

Manejo de excepciones

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

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

Computación asincrónica

Lanzar un thread para que se ejecute asincrónicamente:

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

A lo Scala...

Manejo de null

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

Enfoque imperativo

Enfoque funcional

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

Option[T]: Permite representar la ausencia de respuesta

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

Manejo de error

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

Enfoque imperativo

Enfoque funcional

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

Genera un side-effect

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

Either[T]: Effect para modelar errores

Es puro (sin side-effect)

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

Manejo de excepciones

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

Enfoque imperativo

Enfoque funcional

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

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

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

Computación asincrónica

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

Enfoque imperativo

Enfoque funcional

implicit val executionContext: ExecutionContext = ???

Future {
  println("Thread Running")
}

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

Fácil de componer

implicit val executionContext: ExecutionContext = ???

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

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

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

Effect

Tiene la forma F[T]

type F[T] = Option[T]

Puede tener un valor T o no

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

Devuelve un error E o un resultado T

type F[T] = List[T]

Genera entre 0 y N valores T

(Para algún E)

Programa en F que computa un valor T

type F[T] = Try[T]

Devuelve un valor T o bien una excepción

type F[T] = Future[T]

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

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

Tipos de datos con info extra

Effects y Higher-Order Functions

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

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

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

Ejemplo de uso con Option[A]:

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

For-Comprehension

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

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

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

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

Ejemplo:

Escrito con for-comprehension:

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

Ejercicio

Implementar los servicios de usuario

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

¡ Muchas Gracias !

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: intro to Scala (short edition)

By Cristian Spinetta

Workshop: intro to Scala (short edition)

A short intro to Scala language

  • 488